TuringDB home pagelight logodark logo
  • Discord
  • GitHub
  • Website
TuringDB

Get Started

  • Introduction
  • Get Started
  • Claude Code Skill
  • Commands

Concepts

  • Overview
  • A Columnar Graph Database
  • Versioning System
  • The DataPart System
  • Zero-Locking Architecture
  • Snapshots Isolation

Benchmarks

  • Results Summary
  • Technical Report

Query Language

  • Overview
  • Matching
  • Paths
  • Results and Aggregation
  • Subqueries
  • Functions and Types
  • Writing Data
  • Procedures and Commands
  • CheatSheet

Importing data

  • JSONL
  • CSV
  • Neo4j
  • GML
  • Parquet

Graph Development

  • Create Graph
  • Load Graph
  • Load External data
  • Create a Change
  • List Changes
  • Create Nodes and Edges
  • Update Properties
  • Submit a Change
  • Time Travel
  • Graph Examples

Vector Search

  • Vector Search

Graph Algorithms

  • Shortest Path

Tutorials

  • Example Notebooks

Python SDK

  • Get Started
  • Reference

Troubleshooting

  • Troubleshooting

  • MATCH
  • Filtering inside the pattern
  • Ordering and limiting in MATCH
  • OPTIONAL MATCH
  • WHERE and operators
  • Comparing numbers
  • Joins and Cartesian products
  • Look up nodes by native node ID
Query Language

Matching Patterns

Read data with MATCH, OPTIONAL MATCH and WHERE, join patterns, and look up nodes quickly by native node ID.

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 MATCH clauses 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.name is 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.name

​
Ordering 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.name

Unknown 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:Person tests a node label, e:KNOWS tests an edge type. Combine them with AND, OR and NOT.
  • Pattern predicates: WHERE (p)-[:LIKES]->() keeps the nodes for which the pattern exists. For richer conditions see EXISTS subqueries.
Operators
Comparison=, <>, !=, <, <=, >, >=, and chained comparisons such as 1 < x < 3
Null testsIS NULL, IS NOT NULL
BooleanAND, OR, XOR, NOT, with three-valued logic for null as in Neo4j
Membership and stringsIN, 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 types

Use 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.name

Mixed-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.name

A 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.name

​
Look 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.name

EXPLAIN shows whether it worked. The plan should contain const_scan_nodes rather than scan_nodes:

EXPLAIN MATCH (n)
WHERE n = 0
RETURN n.name

On 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 = 123 is a direct seek. WHERE id(n) = 123 scans every node. Use id(n) only to return the ID.
  • Use UNWIND for batches, not IN. WHERE n IN [...] and WHERE id(n) IN [...] scan every node. The UNWIND [<ids>] AS x MATCH (n) WHERE n = x form 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;
    • x is used only in n = x. Return id(n) rather than x.
  • 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 as WHERE 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 ids binds ids as nodes, so use ids.title or MATCH (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 COMMIT or CHANGE SUBMIT. Read them after that.
  • MERGE_DATAPARTS renumbers 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.

OverviewPaths
githublinkedinyoutubediscord