Lightweight Edges
A lightweight edge is an edge stored as a pair of pointers inside the two vertices it connects, with no record of its own. It therefore carries no properties. A regular edge is a first-class record with its own RID, able to hold arbitrary key/value properties.
The storage shape is declared on the edge type, so every way of creating an edge — SQL, Cypher, Gremlin, the Java API, GraphBatch, the async executor — produces the shape the schema asks for.
CREATE EDGE TYPE Follows LIGHTWEIGHT
database.getSchema().buildEdgeType().withName("Follows").withLightweight(true).create();
When to use them
Use lightweight edges when both of the following hold:
-
the relationship carries no properties, and
-
you mostly traverse from vertices rather than querying the edge type directly.
An edge that needs a weight, a timestamp or any other attribute must be a regular edge.
Querying the edge type directly does work — SELECT FROM <EdgeType> returns the edges — but it costs more than on a regular type, because there are no records to scan and no index to consult: the query reads the vertices instead. If that is your main access pattern, prefer a regular edge type. See Querying a lightweight edge type.
|
Lightweight edges do not make traversal faster. Expanding from a vertex reads the neighbour’s RID straight out of the edge list and never loads the edge record — in either shape. Choose lightweight edges for write throughput and disk footprint, not for read speed. |
Measured performance
Embedded, single writer, warm page cache. Two graph shapes with the same operations. Bulk insert is one transaction per 20K edges; Single insert is one transaction per edge; Delete removes all edges of a sample of vertices.
| Shape | UNIQUE | Bulk insert | Single insert | 1-hop | 2-hop | Delete | Disk per edge |
|---|---|---|---|---|---|---|---|
Regular |
no |
570 ms |
299 ms |
19 ms |
23 ms |
113 ms |
81.6 B |
Regular |
yes |
1,162 ms |
343 ms |
19 ms |
24 ms |
126 ms |
123.3 B |
Lightweight |
no |
494 ms |
195 ms |
21 ms |
25 ms |
71 ms |
45.9 B |
Lightweight |
yes |
494 ms |
188 ms |
19 ms |
22 ms |
56 ms |
45.9 B |
| Shape | UNIQUE | Bulk insert | Single insert | 1-hop | 2-hop | Delete | Disk per edge |
|---|---|---|---|---|---|---|---|
Regular |
no |
123 ms |
35 ms |
2 ms |
101 ms |
217 ms |
59.0 B |
Regular |
yes |
203 ms |
46 ms |
2 ms |
100 ms |
273 ms |
133.9 B |
Lightweight |
no |
35 ms |
19 ms |
2 ms |
100 ms |
147 ms |
24.9 B |
Lightweight |
yes |
47 ms |
19 ms |
2 ms |
100 ms |
147 ms |
24.9 B |
Reading the numbers:
-
Writes get faster as degree grows: bulk insert is 1.2x faster than regular edges at degree 10 and 3.5x faster at degree 100. Single-edge insert is 1.5-1.8x faster, deletion 1.5-1.6x.
-
Disk drops 44-58%, and more against a regular type that also carries a
UNIQUEindex. -
Traversal is unchanged in every configuration — see the note above.
Limitations
-
No properties. Declaring a property on a
LIGHTWEIGHTedge type, or passing properties tonewEdge(), is rejected with a schema error. -
No inherited constraint that needs a record. A lightweight edge is never validated, so a
MANDATORYor default-valued property would be declared and never applied. Since v26.10.2 the schema refuses to let one reach a lightweight type through a super type:CREATE EDGE TYPE Child EXTENDS Parent LIGHTWEIGHTandALTER TYPE Child SUPERTYPE +Parentare rejected whileParentdeclares or inherits such a property, so areALTER TYPE Child WITH lightweight = trueon a type that does, and so is making a property of a super typeMANDATORYor giving it a default while a lightweight edge type sits below it. Optional properties, and ones that only constrain a value that is present (NOT NULL,MIN,MAX,REGEXP), are still inherited.A database whose schema already holds the combination, written by an earlier version, still opens and the constraint stays declared and unenforced. Since v26.10.2 opening it logs a
WARNINGnaming the lightweight type and the property it cannot apply, andCHECK DATABASElists the same finding underwarnings. The message names every such property with its way out: aMANDATORYone is made optional (ALTER PROPERTY Parent.since MANDATORY false), a default-valued one has its default removed (ALTER PROPERTY Parent.weight DEFAULT null, which fills in nothing and is therefore accepted under a lightweight type). Declaring the type withoutLIGHTWEIGHTclears the finding too. -
No records, therefore no usable RID. Every lightweight edge of a type reports the same placeholder
@rid, so you cannot address one by RID. See Querying a lightweight edge type for what this costs.A lightweight edge still answers its metadata wherever metadata is asked for: (since v26.10.2) a result map, the SQL
include()andexclude()methods (include('@rid', '@in', '@out')), and the HTTPgraphandstudioserializers give@cat,@type,@rid,@inand@out, with the same keys as a record-backed edge. In v26.10.1 and earlier a lightweight edge read off the graph answered an empty map there. -
Grants still apply. (Since v26.10.2) the
createRecordanddeleteRecordgrants of aLIGHTWEIGHTedge type are enforced, on every path that creates or deletes such an edge (SQLCREATE EDGEandDELETE, openCypher, Gremlin,Vertex.newEdge(),Edge.delete(),GraphBatch). Deleting a vertex that holds lightweight edges needsdeleteRecordon their edge type, as for a record-backed edge. In v26.10.1 and earlier both grants were ignored on a lightweight type, since no record is written. See Groups. -
Identity is the triple
(edge type, out vertex, in vertex). With no properties there is no observable difference between two lightweight edges of the same type over the same ordered pair, so they are the same edge.
Querying a lightweight edge type
These all work and agree with each other:
SELECT FROM Follows
SELECT count(*) FROM Follows
DELETE FROM Follows
Because a lightweight edge has no record, none of them can read the edge type’s own storage: they read the vertices that hold the edges. Two consequences are worth knowing before you rely on them:
-
count() is not instant.* On a regular type it answers from a stored counter; on a lightweight type no such counter can be correct, so the count reads the graph. A throttledWARNINGin the server log tells you when this happens on a large database. -
LIMITdoes not make it cheap. There is nothing to seek into, so a smallLIMITmay still read a large part of the vertex set.
Anchoring the query on the vertices stays the cheapest way to reach these edges, and is unchanged:
SELECT expand(outE('Follows')) FROM Person
SELECT FROM Follows WHERE @out = #12:0
TRAVERSE … FROM <EdgeType> and TRUNCATE TYPE <EdgeType> work the same way, reading the vertices behind the scenes:
TRAVERSE in, out FROM Follows
TRUNCATE TYPE Follows
|
This applies to the type you name, not to the whole hierarchy: a regular edge type that merely has a lightweight subtype below it truncates normally, clearing its own records and leaving the subtype’s edges untouched, as it would with any other subtype. Since v26.10.1 - earlier versions refused that case too. |
Duplicates
ArcadeDB does not prevent a second lightweight edge over the same ordered pair unless the type declares UNIQUE, because the check costs a scan of the source vertex’s edge list. If an application creates one anyway, the behaviour is well defined and nothing is corrupted:
-
traversals return the edge twice;
-
path uniqueness (Cypher relationship uniqueness, the path procedures) counts each copy as a relationship of its own: a path may walk one copy and then the other, but never the same copy twice. This holds for every Cypher pattern form: fixed-length hops, variable-length (
-[:K*2]-,-[:K*1..3]-), quantified (a)-[:K]-(b{2}), pattern predicates and pattern comprehensions. Since v26.10.2 - earlier versions treated the copies as one edge and dropped such paths. The two copies have no property to tell them apart, so they are told apart by their place in the edge list they were read from; -
comparing or deduplicating relationship values sees each copy as a relationship of its own:
r1 = r2is false for two copies, andDISTINCT r, grouping,UNION,count(DISTINCT r),collect(DISTINCT r)and path equality keep every copy. A CypherDELETE rremoves and counts every copy it is given. ADELETEthat meets a copy reads its remaining rows in before deleting, within the per-query heap limit (arcadedb.queryMaxHeapElementsAllowedPerOp): over the limit the statement fails and deletes nothing. Since v26.10.2 - earlier versions saw the copies as one, and aDELETEover them could leave copies behind (#10008); -
id()of a lightweight edge isnull: it has no record, so there is no position to encode (earlier versions raised an error); -
each
delete()removes one copy, so the delete must be repeated once per copy; -
CHECK DATABASEreports the count underduplicateLightEdges. It is reported, never auto-fixed: the two copies live in the source vertex’s outgoing list and the target’s incoming list, which are repaired in separate passes.
If you need two distinguishable edges between the same pair of vertices, you need a property to tell them apart — which means a regular edge type.
The UNIQUE declaration
UNIQUE means at most one edge of this type per ordered (out, in) pair.
CREATE EDGE TYPE Follows LIGHTWEIGHT UNIQUE
ALTER TYPE Follows WITH unique = true
It is enforced differently depending on the storage shape, and the cost is the opposite of what most people expect:
| Type shape | Mechanism | Insert cost | Disk cost |
|---|---|---|---|
Lightweight |
Scan of the source vertex’s edge list |
None at degree 10, ~35% at degree 100 |
None |
Regular |
Unique index on |
+65% to +100% |
Roughly 2x per edge |
Uniqueness is therefore cheap on lightweight edges and expensive on regular ones. On a regular type the index is created for you and cannot be dropped while the type declares UNIQUE — withdraw the declaration with ALTER TYPE <name> WITH unique = false, which drops the flag and the index together.
Declaring UNIQUE on an already-populated regular type builds the index over the existing data, so it fails with a DuplicatedKeyException naming the offending pair if duplicates are already present. On a lightweight type the flag applies to new edges only; use CHECK DATABASE to inspect what is already stored.
Tuning the edge-list chunk size
Every vertex stores its adjacency list (per direction) as a chain of edge chunks. The first chunk is arcadedb.graph.edgeListInitialChunkSize bytes (default 64) and each further chunk doubles the previous one up to 8192, so the space a vertex allocates is the sum of that series.
|
A smaller first chunk does not necessarily use less space. It takes more chunks to reach the same capacity, and each chunk carries its own record header. The best value depends on the degree distribution, not on whether the edges are lightweight. |
Disk per edge, sweeping the first-chunk size (lightweight | regular):
| First chunk | Degree 2 | Degree 10 | Degree 100 |
|---|---|---|---|
32 B |
72.7 B | 139.6 B |
36.0 B | 86.2 B |
24.9 B | 59.0 B |
64 B (default) |
95.0 B | 128.5 B |
45.9 B | 81.6 B |
24.9 B | 59.0 B |
128 B |
164.5 B | 197.3 B |
36.7 B | 71.4 B |
24.9 B | 57.7 B |
256 B |
304.1 B | 336.9 B |
67.5 B | 99.9 B |
23.6 B | 57.7 B |
512 B |
585.2 B | 618.0 B |
129.4 B | 161.9 B |
26.2 B | 77.3 B |
When to change it:
-
Leave it at the default unless you have measured your degree distribution. There is no value that wins everywhere.
-
Average degree around 10:
128is worth trying. It is better than the default on every axis for both edge shapes — around 15% faster bulk insert, 12% faster delete, and 20% less disk. The default of64is an unlucky fit for this shape: after the chunk header it leaves 51 usable bytes, and a degree-10 adjacency list needs slightly more, so it spills into a second chunk and allocates 192 bytes to hold about 58. -
Very sparse graphs (average degree 2-3): keep the default, or try
32for lightweight edges, where it saves about 24% of the disk. Raising the value here is expensive —128costs over 70% more space at degree 2. -
High degree (100+): the setting barely matters; the doubling reaches the same place regardless of where it starts.
Values below 32 are clamped: a chunk smaller than that cannot hold the header plus one entry.
Low-degree graphs pay the most for chunk allocation — 95 bytes per edge at degree 2 against 45.9 at degree 10 for the same lightweight edges — because each vertex allocates a first chunk per direction with very few edges to amortise it over. That is where this setting earns its keep.
See Settings for how to change it per database.
Migrating from the per-call API
The shape used to be chosen per call, with Vertex.newLightEdge() and the withLightEdges flag on GraphBatch, the remote batch builder, the HTTP batch endpoint and the async executor. Those are deprecated. GraphBatch with withLightEdges(true) now raises an IllegalArgumentException for an edge without properties whose type does not declare LIGHTWEIGHT, because such edges were invisible to the count shortcuts.
Declare LIGHTWEIGHT on the type instead. A per-call flag cannot be honoured by SQL, Cypher, the exporter or the UNIQUE constraint, all of which need to know the shape from the schema; a type that declares it is stored lightweight regardless of what any per-call flag says.
See Also
-
Super-Nodes (Hot Vertices) — how ArcadeDB handles vertices with very high or fast-growing edge counts
-
SQL -
CREATE TYPE— theLIGHTWEIGHTandUNIQUEmodifiers -
GraphBatch — bulk edge import