Read queries run directly against the current graph, with no change to open first.
The examples on this page use a small graph of people, companies and interests. Create it once in a change (see Writing data) if you want to follow along:
CREATE
(alice:Person {name: 'Alice', age: 30, city: 'London', active: true, tags: ['admin', 'vip']}),
(bob:Person {name: 'Bob', age: 35, city: 'London', active: true, tags: ['dev']}),
(carol:Person {name: 'Carol', age: 28, city: 'Paris', active: false, tags: ['dev', 'vip']}),
(dave:Person {name: 'Dave', age: 41, city: 'Paris', active: true}),
(acme:Company {name: 'Acme', founded: 1999}),
(globex:Company {name: 'Globex', founded: 2010}),
(music:Interest {name: 'Music'}),
(alice)-[:KNOWS {since: 2010}]->(bob),
(bob)-[:KNOWS {since: 2015}]->(carol),
(carol)-[:KNOWS {since: 2018}]->(dave),
(alice)-[:WORKS_AT {role: 'CTO'}]->(acme),
(bob)-[:WORKS_AT {role: 'Engineer'}]->(acme),
(carol)-[:WORKS_AT {role: 'Engineer'}]->(globex),
(alice)-[:LIKES]->(music),
(carol)-[:LIKES]->(music)MATCH
MATCH (n)
RETURN count(n)
MATCH (n:Person)
RETURN n.name, n.age
MATCH (n:Person {name: 'Alice', age: 30})
RETURN n.name
MATCH (n:Person:Employee)
RETURN n.name
MATCH (a:Person)-[:KNOWS]->(b:Person)
RETURN a.name, b.name
MATCH (a)-[e:KNOWS {since: 2010}]->(b)
RETURN a.name, e.since, b.name
MATCH (a)<-[e]-(b)
RETURN a.name, b.name
MATCH (a)-[e]-(b)
RETURN a.name, b.name
MATCH (a)-[r:KNOWS|WORKS_AT]->(b)
RETURN a.name, type(r), b.name
MATCH (a {name: 'Alice'})
MATCH (a)-[:KNOWS]->(b)
RETURN b.name(n:Person:Employee)matches nodes that have both labels.(a)-[e]-(b)(or(a)--(b)) ignores direction.[:KNOWS|WORKS_AT]matches either edge type. An edge has exactly one type, so each edge pattern takes a single type or a list of alternatives.type(r)returns the edge type as a string.- Several
MATCHclauses in a row are allowed, and a later one can reuse variables from an earlier one. - Variables are only needed for entities you want to return or reuse.
MATCH (:Person)-[:KNOWS]->(m) RETURN m.nameis valid, and a node or edge may omit its variable when it has a label or type.
RETURN n returns the node’s internal ID, not its properties. Return the properties you need (RETURN n.name, n.age). This is different from Neo4j, where RETURN n returns the whole node.
Filtering inside the pattern
A WHERE can sit inside a node or edge pattern. The variable must be named:
MATCH (a:Person WHERE a.age > 30)-[r:KNOWS WHERE r.since > 2012]->(b)
RETURN b.nameOrdering and limiting in MATCH
As a TuringDB extension, ORDER BY, SKIP and LIMIT can also appear right after the pattern, before RETURN:
MATCH (n:Person)
ORDER BY n.age LIMIT 2
RETURN n.nameUnknown labels and property names are forgiving: MATCH (n:Nope) RETURN n returns no rows and RETURN n.misspelled returns null. Use CALL db.labels(), CALL db.edgeTypes() and CALL db.propertyTypes() to check what exists in a graph.
OPTIONAL MATCH
OPTIONAL MATCH keeps the row when the pattern has no match and returns null for its variables:
MATCH (a:Person)
OPTIONAL MATCH (a)-[:WORKS_AT]->(c:Company)
RETURN a.name, c.name
MATCH (a:Person)
OPTIONAL MATCH (a)-[:KNOWS]->(b)
RETURN a.name, count(b)The second query returns 0 for people who know nobody. A WHERE attached to an OPTIONAL MATCH belongs to the optional part, so rows are kept, as in Neo4j.
Return a property (c.name) or id(c) from an optional node, rather than the bare variable.
WHERE and operators
MATCH (n)
WHERE n:Person
RETURN n.name
MATCH (n:Person)
WHERE n.age >= 18 AND n.city = 'London'
RETURN n.name
MATCH (n:Person)
WHERE n.name STARTS
WITH 'A' OR n.name ENDS
WITH 'e' OR n.name CONTAINS 'ro'
RETURN n.name
MATCH (n:Person)
WHERE n.name IN ['Alice', 'Bob']
RETURN n.name
MATCH (n:Person)
WHERE 'admin' IN n.tags
RETURN n.name
MATCH (n:Person)
WHERE n.tags IS NOT NULL
RETURN n.name
MATCH (n:Person)
WHERE n.active
RETURN n.name
MATCH (n)-[e]->(m)
WHERE e:KNOWS AND e.since > 2012
RETURN n.name, m.name
MATCH (p:Person)
WHERE (p)-[:LIKES]->()
RETURN p.name
MATCH (p:Person)
WHERE NOT (p)-[:KNOWS]->()
RETURN p.name- Label and type tests:
n:Persontests a node label,e:KNOWStests an edge type. Combine them withAND,ORandNOT. - Pattern predicates:
WHERE (p)-[:LIKES]->()keeps the nodes for which the pattern exists. For richer conditions seeEXISTSsubqueries.
| Operators | |
|---|---|
| Comparison | =, <>, !=, <, <=, >, >=, and chained comparisons such as 1 < x < 3 |
| Null tests | IS NULL, IS NOT NULL |
| Boolean | AND, OR, XOR, NOT, with three-valued logic for null as in Neo4j |
| Membership and strings | IN, STARTS WITH, ENDS WITH, CONTAINS |
| Arithmetic | +, -, *, /, %, ^. Integer / truncates and ^ returns a Double |
| Concatenation | + and || join strings and lists |
Property constraints in a pattern, MATCH (n {name: 'Matt', age: 20}), are exactly equivalent to the same equality tests in WHERE.
Use n.prop IS NOT NULL to test for a property, and STARTS WITH, ENDS WITH and CONTAINS to match strings. Regular expressions (=~) and exists(n.prop) are not yet supported.
Comparing numbers
Equality compares numbers of the same kind. An Integer compared with a Double using =, or two Doubles compared with =, is reported when the query is analysed:
MATCH (n:Person) WHERE n.age = 30.0 RETURN n.name
ANALYZE_ERROR: Operands are not valid or compatible typesUse range comparisons for Doubles, or convert the value:
MATCH (n:Person)
WHERE n.age > 29.5 AND n.age < 30.5
RETURN n.name
MATCH (n:Person)
WHERE n.age = toInteger(30.0)
RETURN n.nameMixed-type <, > and arithmetic work as expected. Division by zero returns an execution error, and Int64 arithmetic wraps around on overflow.
Joins and Cartesian products
Comma-separated patterns in a MATCH produce either a join or a Cartesian product, depending on whether they share a variable.
// Cartesian product: every Person with every Company (N x M rows)
MATCH (p:Person), (c:Company)
RETURN p.name, c.name
// Join: a shared variable links the patterns
MATCH (a:Person)-[:KNOWS]->(b), (b)-[:WORKS_AT]->(c)
RETURN a.name, b.name, c.name
// Join on a shared node: people who like the same thing
MATCH (a:Person)-->(i:Interest)<--(b:Person)
WHERE a <> b
RETURN a.name, b.name, i.name
// Join on a value through WHERE (hash join)
MATCH (a:Person), (b:Person)
WHERE a.city = b.city AND a <> b
RETURN a.name, b.nameA Cartesian product of N nodes by M nodes produces N × M rows. Always constrain it with WHERE or LIMIT before running it on a large graph.
Connected paths and independent patterns can be mixed in one query. Shared variables create joins and unrelated patterns create Cartesian products:
MATCH (a:Person)-->(i:Interest), (c:Company)
WHERE c.name = 'Acme'
RETURN a.name, i.name, c.nameLook up nodes by native node ID
Every node has a native node ID, the integer you get from RETURN n or id(n). When you already know which nodes you want, compare the node variable itself with the ID, written as a literal. The planner turns this into a constant scan of exactly those nodes:
MATCH (n)
WHERE n = 0
RETURN n.name
MATCH (n)-[:KNOWS]->(m)
WHERE n = 0
RETURN m.name
UNWIND [0, 1, 2] AS x
MATCH (n)
WHERE n = x
RETURN id(n), n.nameEXPLAIN shows whether it worked. The plan should contain const_scan_nodes rather than scan_nodes:
EXPLAIN MATCH (n)
WHERE n = 0
RETURN n.nameOn large graphs this is orders of magnitude faster than looking nodes up again through a key property such as n.uuid. The rules that keep it fast:
- Compare the variable, not the function.
WHERE n = 123is a direct seek.WHERE id(n) = 123scans every node. Useid(n)only to return the ID. - Use
UNWINDfor batches, notIN.WHERE n IN [...]andWHERE id(n) IN [...]scan every node. TheUNWIND [<ids>] AS x MATCH (n) WHERE n = xform is rewritten at plan time into one constant scan, which costs roughly a microsecond per node. - The batch form needs an exact shape:
- the list is a literal written in the query text (build it in Python:
f"UNWIND {ids} AS x ..."), not a variable,collect(...),range(...)or a list property; - its elements are distinct integers, so deduplicate first;
xis used only inn = x. Returnid(n)rather thanx.
- the list is a literal written in the query text (build it in Python:
- Fetch IDs once and reuse them. Look nodes up with whatever filter you need (
MATCH (n:Person {name: 'Alice'}) RETURN id(n)), keep the IDs in your application, and address the nodes by ID from then on. - Keep IDs constant. Use literal IDs rather than IDs computed per row, and match one node per
MATCH. Forms such asWHERE a = p[0] AND b = p[1]are not rewritten into a constant scan, so they run as a Cartesian product of two full node scans, which is expensive on large graphs. To connect two known nodes, put both IDs in as literals, one pair per query:MATCH (a) WHERE a = 5 MATCH (b) WHERE b = 7. For bulk edges, see Parquet import. - Vector search results are already nodes.
VECTOR SEARCH ... YIELD idsbindsidsas nodes, so useids.titleorMATCH (ids)-[...]->(...)directly. See Vector search.
Stability. The native IDs of existing nodes are stable across queries, change submits and server restarts, so it is safe to cache them. Two exceptions:
- Nodes created in an open change get their final IDs at
COMMITorCHANGE SUBMIT. Read them after that. MERGE_DATAPARTSrenumbers all nodes. Re-fetch any cached IDs afterwards.
Keep a business key as a property as well, for durable identity outside TuringDB. Use id(n) for node IDs. For edges, MATCH ()-[r]->() WHERE id(r) = 0 RETURN startNode(r), endNode(r) works.

