Graph OLAP Engine
| Available since ArcadeDB v26.4.1. |
The Graph OLAP Engine maintains a read-optimized, columnar representation of your graph alongside the live OLTP data. It uses Compressed Sparse Row (CSR) encoding and flat primitive arrays to deliver 5x–400x speedups on analytical workloads — multi-hop traversals, graph algorithms, and property aggregations — without sacrificing transactional safety.
Why Graph OLAP?
ArcadeDB’s OLTP engine is optimized for point lookups and ACID transactions. Analytical workloads — PageRank, community detection, multi-hop traversals — access millions of edges in tight loops. The row-oriented, pointer-chasing nature of OLTP storage causes cache misses, object overhead, and GC pressure.
The OLAP engine solves this by encoding graph topology as flat int[] arrays and properties as typed columns:
-
Sequential memory access — cache-line friendly, no pointer chasing
-
Zero object allocation — no GC pressure during traversal
-
SIMD-friendly — enables JVM vectorized operations
-
9x more compact — flat arrays vs. Java object overhead
Graph Analytical View (GAV)
A Graph Analytical View is a named, schema-persisted OLAP snapshot of selected vertex types, edge types, and properties.
GraphAnalyticalView gav = GraphAnalyticalView.builder(database)
.withName("social")
.withVertexTypes("Person", "Company")
.withEdgeTypes("FOLLOWS", "WORKS_AT")
.withProperties("name", "age", "status")
.withUpdateMode(UpdateMode.SYNCHRONOUS)
.build();
Named views are persisted in schema.json and automatically restored on database restart.
As of ArcadeDB v26.9.1, a READY view’s CSR snapshot itself can also be persisted to disk at a clean database close and reused on the next open without rescanning the graph; see CSR Persistence.
SQL
Creating a view
CREATE GRAPH ANALYTICAL VIEW social
VERTEX TYPES (Person, Company)
EDGE TYPES (FOLLOWS, WORKS_AT)
PROPERTIES (name, age, status)
UPDATE MODE SYNCHRONOUS
All clauses after the view name are optional. A minimal view covering the entire graph:
CREATE GRAPH ANALYTICAL VIEW fullGraph
Use IF NOT EXISTS to avoid errors if the view already exists:
CREATE GRAPH ANALYTICAL VIEW social IF NOT EXISTS
VERTEX TYPES (Person)
EDGE TYPES (FOLLOWS)
UPDATE MODE SYNCHRONOUS
You can also materialize edge properties (e.g., weights):
CREATE GRAPH ANALYTICAL VIEW weighted
VERTEX TYPES (City)
EDGE TYPES (ROAD)
EDGE PROPERTIES (distance, toll)
UPDATE MODE SYNCHRONOUS
COMPACTION THRESHOLD 50000
Altering a view
Change the update mode or compaction threshold of an existing view:
ALTER GRAPH ANALYTICAL VIEW social UPDATE MODE ASYNCHRONOUS
ALTER GRAPH ANALYTICAL VIEW social COMPACTION THRESHOLD 20000
Rebuilding a view
Force a full rebuild of the CSR snapshot:
REBUILD GRAPH ANALYTICAL VIEW social
Dropping a view
DROP GRAPH ANALYTICAL VIEW social
DROP GRAPH ANALYTICAL VIEW IF EXISTS social
Listing views
SELECT FROM schema:graphAnalyticalViews
Builder Options
| Method | Description | Default |
|---|---|---|
|
Named registration + schema persistence |
anonymous |
|
Filter to specific vertex types |
all |
|
Filter to specific edge types |
all |
|
Materialize specific vertex properties |
all |
|
Materialize edge properties (e.g., weights) |
none |
|
OFF, SYNCHRONOUS, or ASYNCHRONOUS |
OFF |
|
Rebuild CSR after N accumulated delta edges |
10,000 |
Async Build for Large Graphs
For large graphs, use buildAsync() to avoid blocking the calling thread:
GraphAnalyticalView gav = GraphAnalyticalView.builder(database)
.withName("large-graph")
.withUpdateMode(UpdateMode.ASYNCHRONOUS)
.buildAsync();
// Wait for build completion
boolean ready = gav.awaitReady(30, TimeUnit.SECONDS);
Update Modes
The GAV supports three synchronization modes between OLTP and OLAP:
| Mode | Behavior | Staleness | Use Case |
|---|---|---|---|
OFF |
Marks view STALE on commit; requires manual rebuild |
Until rebuild |
Batch analytics, static snapshots |
SYNCHRONOUS |
Applies an overlay on each commit |
Zero |
Real-time analytics, consistent reads |
ASYNCHRONOUS |
Triggers background rebuild on commit |
Brief BUILDING window |
Large graphs, tolerable brief inconsistency |
In SYNCHRONOUS mode, the engine captures transaction deltas (new/deleted vertices, added/removed edges, property changes) and merges them into an immutable overlay on top of the base CSR. Readers always see a consistent snapshot via an atomic volatile reference swap. When the overlay accumulates too many changes (configurable threshold, default 10,000 edges), a background compaction rebuilds the full CSR.
CSR Persistence
| Available since ArcadeDB v26.9.1. |
Before v26.9.1, a named view’s CSR was never itself persisted: only its definition (types, properties, update mode) was, in schema.json. Every database open therefore rebuilt the CSR from a full graph scan, and because shutdown() waits for any in-flight rebuild, the cost of that scan landed on close() rather than open() - at 1M vertices, closing a database with a view took over 4 seconds even for a session that ran no query at all.
A READY view (with no pending overlay changes) now writes its CSR to disk at a clean database close, alongside a freshness certificate: the database’s last committed transaction id at the time the CSR was built. On the next open, if a persisted file plausibly applies (see arcadedb.gavPersistCsr below), the view is marked READY immediately, but the file itself is not read yet - see Lazy Restore. That deferred read either:
-
finds the database’s current last committed transaction id still matches the certificate - nothing was committed to the database in between, so the persisted CSR is loaded from disk as-is instead of being rebuilt; or
-
finds a mismatch - anything was committed in between, to any type, not only the ones the view covers - and falls back to the previous behavior: an async rebuild.
This means the speedup only applies to reopening a database that was not written to since its last clean close. A database that receives writes between every session still pays the rebuild on every open, exactly as before v26.9.1.
Persistence is controlled by the arcadedb.gavPersistCsr database-level setting (default true). Set it to false to skip writing the CSR file, for example to avoid its disk footprint or the extra write at close.
Serving queries against a stale CSR while a delta is applied in the background (the way SYNCHRONOUS mode’s overlay already does for live updates) is not part of this: a CSR’s contiguous array layout makes partial coverage expensive to retrofit, so an invalidated certificate always means a full rebuild, not a partial one.
Lazy Restore
| Available since ArcadeDB v26.9.1. |
A persisted CSR that plausibly applies is not read from disk during open(). Reading and deserializing it is a real, if bounded, cost - up to roughly a second at 10M vertices - and a session that opens a database and never queries the view got nothing for paying it. Instead, open() marks the view READY immediately without touching the file, and the actual read is deferred to whichever of these happens first:
-
a query that actually needs the view’s data; or
-
an explicit call to the view’s
awaitReady()method.
Either trigger re-verifies the freshness certificate from scratch at that point (not the one implied at open() time), so a commit that lands after open() but before the first query is still caught correctly and falls back to a rebuild rather than serving stale data. A session that opens and closes a database without ever touching the view now costs what the no-view baseline costs, not what a restore costs - close() does not wait for a restore that was never triggered either.
The arcadedb.gavRestoreAwaitTimeout database-level setting (default 0) forces the wait at open() time instead, for either the deferred restore or a full rebuild: set it to a positive number of milliseconds to trade a slower open() for the view being immediately usable by the query that triggered the reopen, rather than the first query racing (and possibly missing) it.
How CSR Works
The graph topology is stored as two pairs of arrays (forward for outgoing edges, backward for incoming):
Forward CSR (outgoing edges): offsets: [0, 3, 5, 8, ...] -- one entry per vertex + sentinel neighbors: [1, 5, 7, 2, 6, ...] -- dense neighbor IDs, contiguous per source Outgoing neighbors of vertex v = neighbors[offsets[v] .. offsets[v+1]) Out-degree of vertex v = offsets[v+1] - offsets[v] -- O(1)
This layout enables sequential memory access (cache-line friendly) and O(1) degree lookups.
Columnar Property Storage
Properties are stored as typed flat arrays — int[], long[], double[], or dictionary-encoded int[] for strings.
Each column has a compact null bitmap (1 bit per vertex).
Dictionary encoding maps unique string values to integer codes, achieving near-100% compression for low-cardinality fields.
Edge Properties and Pending Changes
| Behavior changed in ArcadeDB v26.9.1. |
Edge property columns (EDGE PROPERTIES (…), used as weights by the shortest-path and spanning-tree algorithms) are laid out in the order the graph had when the view was built.
In SYNCHRONOUS mode a commit is absorbed into a pending-changes overlay rather than rebuilding, so from that moment the neighbour list and the columns describe two different orderings.
Before v26.9.1 the view resolved this by reporting no edge properties at all as soon as an overlay existed, and every weighted algorithm silently went back to reading edge records - after every single commit. The view now reconciles the two instead: an edge already in the base graph keeps answering from its column, and an edge the overlay added answers from the value recorded when it was committed. Weighted algorithms therefore keep the fast columnar path while a view is being updated.
Two cases remain that the view cannot answer, and for those the algorithm reads the edge records directly - exact, only slower:
-
several parallel edges join the same two vertices and only some of them were deleted, since the overlay counts deletions per vertex pair and cannot say which of the parallel edges went;
-
an existing edge’s property value was changed, until the rebuild that refreshes the columns completes. Adding and deleting edges does not have this effect; only changing a property of an edge already in the base graph does.
Memory Usage
The OLAP representation is significantly more compact than the OLTP equivalent:
-
CSR topology: ~8 bytes per edge (bidirectional)
-
Node ID mapping: ~8 bytes per vertex
-
Columnar properties: 4–8 bytes per vertex per column
-
Null bitmaps: 1 bit per vertex per column
Example: for a graph with 500K vertices and 8M edges, the GAV uses 134.6 MB compared to an estimated ~1.2 GB for the OLTP representation — 9.3x more compact.
long bytes = gav.getMemoryUsageBytes();
Graph Algorithms
The module includes parallelized graph algorithms that operate directly on CSR arrays with zero GC pressure:
| Algorithm | Description |
|---|---|
PageRank |
Pull-based, parallel, configurable damping factor and iterations |
Connected Components |
Min-label propagation for weakly connected components |
BFS |
Breadth-first search with distance arrays |
SSSP (Dijkstra) |
Single-source shortest path for weighted graphs |
Label Propagation |
Community detection |
Triangle Counting |
Count 3-cliques in the graph |
Local Clustering Coefficient |
Per-vertex clustering coefficients |
GraphAlgorithms algos = new GraphAlgorithms();
// PageRank (20 iterations, damping 0.85)
double[] ranks = algos.pageRank(gav, 20, 0.85);
// Connected Components
int[] components = algos.connectedComponents(gav);
// BFS from a source vertex
int[] distances = algos.bfs(gav, sourceNodeId);
Algorithms and Pending Changes
| Behavior changed in ArcadeDB v26.9.1. |
In SYNCHRONOUS mode a committed change is absorbed into a pending-changes overlay rather than rebuilding the view.
The overlay keeps the slot of a deleted vertex and numbers an added vertex above the base graph, so from that moment the view’s vertex count and the range of its internal vertex numbers stop being the same figure.
Before v26.9.1 the algo.* procedures took them for one and the same, with two consequences:
-
a vertex added after the view was built could be missing from the answer of every procedure, with no error to say so;
-
once the additions outnumbered the deletions, the CSR-accelerated procedures (
algo.wcc,algo.pagerank,algo.labelpropagation,algo.localClusteringCoefficient,algo.bfs) failed with anArrayIndexOutOfBoundsException- one added vertex was enough.
Both are fixed. The vertices a view holds are renumbered onto a gapless range before any algorithm sees them, so every algo.* procedure answers for exactly the vertices that are live at the time of the call - the added ones included, the deleted ones excluded.
While a view is holding pending changes the whole-graph procedures also stop using the parallel kernels listed above, which read the base CSR arrays directly and know nothing of the overlay: they would otherwise answer for the graph as it stood at the last build. Those calls still run off the view’s adjacency, single-threaded rather than parallel, and the compaction that folds the overlay back into the base graph (by default after 10,000 pending edges) restores the parallel path.
Query Planner Integration
The Cypher query planner automatically detects ready GAVs and substitutes OLTP traversal operators with CSR-based operators when:
-
A named GAV is registered and in READY state
-
The GAV covers the required vertex and edge types
-
The query does not return edge variables as first-class records (edges in CSR have no RID; edge properties are fully supported)
No query changes are needed — the optimizer transparently accelerates matching traversal patterns.
Lifecycle
// Check status
if (gav.isReady()) { /* safe to query */ }
// Status values: NOT_BUILT, BUILDING, READY, STALE
Status status = gav.getStatus();
// Drop (removes from registry + schema)
gav.drop();
// Shutdown (release resources, schema definition persists)
gav.shutdown();
Benchmark Results
On a graph with 500K vertices and ~8M edges:
| Benchmark | OLTP | OLAP | Speedup |
|---|---|---|---|
1-hop count |
6.9 µs |
1.2 µs |
5.7x |
2-hop |
101.4 µs |
5.1 µs |
19.8x |
3-hop |
1,037 µs |
56.4 µs |
18.4x |
5-hop |
194,046 µs |
5,141 µs |
37.7x |
Shortest Path |
394 ms/pair |
7.5 ms/pair |
52.8x |
PageRank (20 iter) |
124,563 ms |
316 ms |
394.2x |
Connected Components |
5,591 ms |
197 ms |
28.4x |
Label Propagation |
62,619 ms |
645 ms |
97.1x |
Limitations
-
CSR uses
int[]arrays — maximum ~2.1 billion vertices per bucket and ~2.1 billion edges per direction -
Edges in CSR do not carry their own RID; the Cypher query planner falls back to OLTP only when the query returns an edge variable as a first-class record (e.g.,
RETURN r). Edge properties are fully supported viawithEdgeProperties() -
Dictionary encoding applies only to string properties
-
Initial build requires a full scan of selected vertex/edge types. A rebuild triggered by an invalidated persisted CSR certificate (or by
OFFmode, or by exceeding theSYNCHRONOUS/ASYNCHRONOUScompaction threshold) also requires a full scan - only a clean-close-to-unchanged-reopen cycle skips it