Introduction
ArcadeDB provides native support for the Cypher query language through a new, high-performance implementation built from the ground up. This native OpenCypher module uses an ANTLR4-based parser with the official Cypher v25 grammar, providing excellent compatibility and performance.
The query language identifiers opencypher and cypher are interchangeable: both run on the same native OpenCypher engine.
|
Key Features
The native OpenCypher implementation provides:
-
High Performance: Native direct execution, comparable to native SQL
-
Full Read/Write Support: MATCH, CREATE, MERGE, SET, DELETE with automatic transaction handling
-
Advanced Pattern Matching: Variable-length paths with path modes (WALK, TRAIL, ACYCLIC), OPTIONAL MATCH, multiple MATCH clauses
-
Query Composition: UNION/UNION ALL for combining results, WITH for query chaining
-
Procedures and Profiling: CALL for invoking built-in procedures and custom functions, PROFILE for performance analysis
-
Rich Function Library: 23+ native Cypher functions plus bridge to 100+ ArcadeDB SQL functions
-
Cost-Based Optimizer: Intelligent query planning with index selection and join ordering
-
100% Test Coverage: Comprehensive test suite with 295+ passing tests
Using OpenCypher
To execute OpenCypher queries, use opencypher as the query language identifier.
From Java API:
ResultSet result = database.query("opencypher",
"MATCH (p:Person) WHERE p.age >= $minAge RETURN p.name, p.age ORDER BY p.age",
"minAge", 25);
Through Neo4j BOLT Protocol:
Connect using any Neo4j driver via the BOLT protocol:
Driver driver = GraphDatabase.driver("bolt://localhost:7687", AuthTokens.basic("root", "password"));
Session session = driver.session(SessionConfig.forDatabase("mydatabase"));
Result result = session.run("MATCH (p:Person) WHERE p.age >= 25 RETURN p.name, p.age ORDER BY p.age");
Through PostgreSQL Driver:
Prefix your query with {opencypher}:
"{opencypher} MATCH (p:Person) WHERE p.age >= 25 RETURN p.name, p.age ORDER BY p.age"
Through HTTP/JSON API (GET - idempotent queries):
curl "http://localhost:2480/query/graph/opencypher/MATCH%20(p:Person)%20RETURN%20p"
Through HTTP/JSON API (POST - updates):
curl -X POST "http://localhost:2480/command/graph" \
-d "{'language': 'opencypher', 'command': 'CREATE (p:Person {name: \"Alice\", age: 30})'}"
Using Record IDs (RIDs)
You can use ArcadeDB’s RecordIDs (RID) in Cypher queries to start from a specific vertex. RIDs in Cypher are always strings, so they must be contained between single or double quotes.
MATCH (m:Movie)<-[a:ACTED_IN]-(p:Person) WHERE id(m) = '#1:0' RETURN *
Parameters
OpenCypher supports parameterized queries using the $paramName syntax:
MATCH (p:Person) WHERE p.age >= $minAge AND p.city = $city RETURN p
Parameters are passed as key-value pairs after the query string:
ResultSet result = database.query("opencypher",
"MATCH (p:Person) WHERE p.age >= $minAge RETURN p",
"minAge", 25);
Automatic Transaction Handling
A write statement (CREATE, SET, DELETE, MERGE, REMOVE, FOREACH, a write procedure) automatically handles its transaction:
-
If no transaction is active, the whole statement runs in one transaction, committed when the statement has run to its end, as over HTTP and as an implicit transaction in Neo4j. A statement that fails leaves nothing behind, and one that meets a concurrent conflict is run again from the committed state, up to
arcadedb.txRetriestimes (since v26.11.1; before, each write clause committed its own transaction row by row, so a failing statement kept the writes made before the error) -
If a transaction is already active, the statement runs in it and leaves the commit or rollback to its owner
-
A
CALL { … } IN TRANSACTIONSsubquery still commits each batch on its own, so a statement using one is not run again on a conflict
// Automatic transaction - the whole statement commits once it completes
database.command("opencypher", "CREATE (n:Person {name: 'Alice'})");
// Manual transaction - you control commit/rollback
database.transaction(() -> {
database.command("opencypher", "CREATE (a:Person {name: 'Alice'})");
database.command("opencypher", "CREATE (b:Person {name: 'Bob'})");
database.command("opencypher", "MATCH (a:Person {name: 'Alice'}), (b:Person {name: 'Bob'}) CREATE (a)-[:KNOWS]->(b)");
// Commits when lambda completes successfully
});
Performance Considerations
The native OpenCypher implementation includes a cost-based query optimizer that:
-
Automatically selects optimal indexes for node lookups. When several predicates on the same node can use an index (an equality, an
INlist, anORof equalities or a range), each one is costed and the cheapest drives the lookup, so aUNIQUEindex resolved by its whole key is preferred over a non-unique one whatever the properties are called or which operator they use. Two candidates of the same cost are decided by the order the query writes them in (since v26.11.1) -
Uses range index scans for inequality predicates (
>,<,>=,⇐) -
Drives a single-hop relationship pattern from the edge type’s own index when the
WHEREclause pins the relationship’s indexed properties and neither endpoint is selective, e.g.MATCH ()-[t:TRANSFER]→() WHERE t.transactionId IN ['A', 'B'] RETURN t. An equality, anINlist or anORof equalities on the same property all qualify, and a leading prefix of a composite index is enough: with an index on(transactionId, date), filteringtransactionIdalone walks that prefix of the index instead of scanning every edge. The plan shows it asMATCH EDGE BY INDEX -
Optimizes join order based on cardinality estimation
-
Detects bounded patterns for efficient relationship traversal
-
Joins the parts of a pattern no relationship connects -
MATCH (a:Person), (c:City), or twoMATCHclauses - on theWHEREconjuncts that relate them instead of crossing every row with every row (since v26.10.1). A conjunct that reads one part alone narrows that part before the join. Cypher does not fix the order conjuncts are evaluated in, so a conjunct that reads one part, like either side of a join key, is evaluated once per row of that part rather than per pair, and ahead of the conjuncts written before it:rand()there is drawn per row, and an expression that can fail, such as1 / 0, can fail on a row the written order would have skipped. Ac.property = expressionconjunct over an indexed property seeks the index once per row of the other part (IndexNestedLoopJoinin the plan), which holds nothing in memory; any otherleft = rightconjunct is answered by a hash join (ValueHashJoin). Only parts nothing relates are crossed (CartesianProduct). The rows a join buffers count againstarcadedb.queryMaxHeapElementsAllowedPerOpand reserve their heap from the budget all the running queries share,arcadedb.queryMaxHeapRAM, and past a few thousand rows a query that only reads, or only adds entities (CREATE,MERGEwithoutSETactions), holds them by record identity rather than in full ([compact buffer]in the plan)
For best performance:
-
Create indexes on frequently queried properties
-
Use labeled nodes in MATCH patterns for faster type filtering
-
Prefer specific property constraints over full scans
-
Use parameters for queries executed multiple times (enables query plan caching). Since v26.10.2 queries that differ only in the literal values they embed share a cached plan too (see the
queryLiteralParameterizationsetting), but parameters also keep values out of the query text