MATCH — Finding Patterns in the Graph
MATCH describes a pattern to find in the graph. Every successful
match produces one row, with variables bound to concrete
nodes or
relationships. A query can chain many
MATCH clauses in sequence — each reads the rows produced by the
previous one.
New to pattern matching? Start with the Ten-Minute Tour → Find something, then come back for the full reference.
Overview
| You want to… | Pattern |
|---|---|
| Match every node | MATCH (n) |
| Match every node with a label | MATCH (n:Person) |
| Match by label + properties | MATCH (n:Person {name: 'Ada'}) |
| Match a relationship | MATCH (a)-[:KNOWS]->(b) |
| Match any type, any direction | MATCH (a)-[r]-(b) |
| Left-join shape | OPTIONAL MATCH |
| Variable-length hops | MATCH (a)-[:R*1..3]->(b) |
| Bind the whole path | MATCH p = (a)-[:R]->(b) |
Node patterns
Start with the simplest shape — a single node.
MATCH (n) RETURN nOne row per node in the graph. Variables are local to the query — n
only exists between MATCH and RETURN.
Filter by label
MATCH (n:User) RETURN n;
MATCH (n:User:Admin) RETURN n // must have BOTH labelsMultiple labels on the pattern narrow the match — the node must carry every listed label. See Nodes for the rules on labels, case, and conventions.
Inline property filter
Inline maps are a shorthand for equality-only filtering. They're
equivalent to a WHERE clause on each property.
MATCH (n:User {name: 'alice'}) RETURN n;
MATCH (n:User {name: 'alice', age: 42}) RETURN n
;// Equivalent to:
MATCH (n:User)
WHERE n.name = 'alice' AND n.age = 42
RETURN nInline maps only express equality. For ranges, regex, or null checks,
move the predicate into WHERE.
Anonymous node
If you don't need the node variable, drop it:
MATCH (:User)-[:FOLLOWS]->(b) RETURN bThe anonymous form is handy in long patterns where only endpoints matter.
Relationship patterns
A relationship pattern always has two endpoints, a direction (or its absence), and optionally a type and properties.
// Outgoing (src → dst)
MATCH (a)-[r:FOLLOWS]->(b) RETURN a, r, b
;// Incoming (src ← dst)
MATCH (a)<-[r:FOLLOWS]-(b) RETURN a, r, b
;// Undirected — matches either direction, once
MATCH (a)-[r:KNOWS]-(b) RETURN a, r, b
;// Anonymous relationship variable (we don't need \`r\`)
MATCH (a)-[:FOLLOWS]->(b) RETURN a, b
;// Any type, any direction
MATCH (a)-[r]-(b) RETURN type(r), count(*)
;// Inline properties on the relationship
MATCH (a)-[r:FOLLOWS {since: 2020}]->(b) RETURN a, r, bDirection on MATCH is optional. On CREATE and
MERGE it is mandatory.
Multiple relationship types
Use | to match any of several types:
MATCH (a)-[r:FOLLOWS|KNOWS]->(b)
RETURN type(r), count(*)Multi-hop patterns
Chain relationships to traverse further.
// Friends of friends
MATCH (a:User)-[:FOLLOWS]->(b)-[:FOLLOWS]->(c)
RETURN a.name AS who, c.name AS two_hops_awayIntermediate nodes can still be labelled and filtered:
MATCH (p:Person)-[:WORKS_AT]->(c:Company)-[:IN]->(:City {name: 'Amsterdam'})
RETURN p.name, c.nameFor unknown-length traversals (1 to N hops) see Paths → variable-length relationships.
Multiple patterns (cross-product)
Multiple comma-separated patterns produce a Cartesian product — one row per combination.
MATCH (a:User {id: 1}), (b:User {id: 2})
CREATE (a)-[:FOLLOWS]->(b)Disconnected patterns are idiomatic when you want two endpoints for a
CREATE or MERGE that follows. In
a pure read query, they're usually a mistake:
// N * M rows — probably not what you want
MATCH (a:User), (b:User) RETURN a, bFor that same shape connected by a relationship, prefer:
MATCH (a:User)-[:FOLLOWS]->(b:User) RETURN a, bOptional match
OPTIONAL MATCH is the graph equivalent of a SQL left join. When the
pattern matches, variables are bound. When it doesn't, they are null
— but the row from the previous clause still survives.
MATCH (a:User)
OPTIONAL MATCH (a)-[:FOLLOWS]->(b)
RETURN a.name, b.nameUsers with no outgoing :FOLLOWS edge still appear, with b.name = null.
OPTIONAL MATCH with aggregation
Very common pattern: count related entities per node, including zero.
MATCH (u:User)
OPTIONAL MATCH (u)-[:WROTE]->(p:Post)
RETURN u.name AS user, count(p) AS posts
ORDER BY posts DESCcount(p) — not count(*) — is crucial here: the optional miss
binds p to null, and count(expr) skips nulls. count(*) would
count the row and incorrectly yield 1 for users with no posts. See
count.
Chained OPTIONAL MATCH
MATCH (u:User {id: $id})
OPTIONAL MATCH (u)-[:OWNS]->(repo:Repo)
OPTIONAL MATCH (repo)-[:HAS_ISSUE]->(i:Issue {status: 'open'})
RETURN u.name, collect(DISTINCT repo.name) AS repos, count(i) AS open_issuesEach OPTIONAL MATCH is independent — a missing repo doesn't stop the
next optional from running.
Path binding
Bind the whole traversal to a variable with p = …. See also
Paths.
MATCH p = (a)-[r:FOLLOWS]->(b)
RETURN p,
path.length(p) AS hops,
path.nodes(p) AS via,
path.edges(p) AS relspath.length(p) is the hop count; path.nodes(p) and path.edges(p) return
lists (see List Functions).
Progressive patterns
A brief tour, same shape, more useful at each step. Each example adds one idea to the last.
1. Just the pattern
MATCH (u:User)-[:FOLLOWS]->(other:User)
RETURN u, otherOne row per FOLLOWS edge. Returns whole nodes.
2. Project properties
MATCH (u:User)-[:FOLLOWS]->(other:User)
RETURN u.handle AS follower, other.handle AS leaderCleaner for downstream consumers — only the fields that matter.
3. Filter
MATCH (u:User)-[:FOLLOWS]->(other:User)
WHERE u.country = other.country
RETURN u.handle, other.handleSame-country follows only — the predicate references both ends of the relationship.
4. Order and paginate
MATCH (u:User)-[:FOLLOWS]->(other:User)
WHERE u.country = other.country
RETURN u.handle, other.handle
ORDER BY u.handle
LIMIT 505. Aggregate
MATCH (u:User)-[:FOLLOWS]->(other:User)
WHERE u.country = other.country
RETURN u.handle, count(other) AS same_country_follows
ORDER BY same_country_follows DESC
LIMIT 10Each step is one more clause. Users who have never written Cypher often try to start at step 5 — starting at step 1 and adding a clause at a time is faster and catches mistakes earlier.
Common patterns
Lookup by unique property
MATCH (u:User {email: $email})
RETURN u
LIMIT 1If you have not added a uniqueness constraint, LIMIT 1
is a belt-and-braces guard against duplicates.
Filter chain
MATCH (p:Product)-[:IN]->(c:Category {slug: $cat})
WHERE p.price <= $max AND p.in_stock
RETURN p
ORDER BY p.price ASC
LIMIT 20Two-sided match
MATCH (src:User {id: $from}), (dst:User {id: $to})
MATCH (src)-[:FOLLOWS*1..3]->(dst)
RETURN src, dstUseful with shortest paths.
Related entities in one hop
MATCH (p:Person {id: $id})-[:WORKS_AT]->(c:Company)
RETURN p.name, collect(c.name) AS companiesRelationship existence check
Use EXISTS { pattern } to filter without
an extra row:
MATCH (u:User)
WHERE EXISTS { (u)-[:FOLLOWS]->() }
RETURN uEdge cases
Empty graph
MATCH (:Unknown) on an empty graph succeeds with zero rows. On a
populated graph without any node of that label, it fails at analysis:
Unknown label :Unknown. See Troubleshooting.
Self-loops
A node connected to itself:
MATCH (a)-[:FOLLOWS]->(a) RETURN aDuplicate rows
A node reached via two different paths produces two rows. Use
DISTINCT on the RETURN if you only want
one:
MATCH (a:User)-[:FOLLOWS]->(b)-[:FOLLOWS]->(c)
RETURN DISTINCT a, cType mismatch in inline filter
Inline maps are strictly typed. {id: 1} does not match a node with
{id: '1'} — see Troubleshooting.
Symmetric-pair deduplication
An undirected (a)-[:KNOWS]-(b) match produces both (alice, bob)
and (bob, alice) rows. Filter with
id() to keep exactly
one representative per unordered pair:
MATCH (a:Person)-[:KNOWS]-(b:Person)
WHERE id(a) < id(b)
RETURN a.name, b.nameSame variable in two positions
A variable can appear multiple times in a pattern — every occurrence must bind to the same entity. Useful for detecting cycles:
MATCH (a)-[:FOLLOWS]->(b)-[:FOLLOWS]->(a)
RETURN a.name, b.nameRelationships within a single pattern must use distinct variable names, even when the type is the same:
// Invalid: r reused across two relationships
MATCH (a)-[r]->(b)-[r]->(c) RETURN a, b, c
;// Valid
MATCH (a)-[r1]->(b)-[r2]->(c) RETURN a, b, cSee Relationships → edge cases.
Property pattern on both sides
Filters in an inline map apply to that one node only. Filtering both
endpoints uses the shorthand twice, or drops into WHERE for clarity:
MATCH (a:User {country: 'NL'})-[:FOLLOWS]->(b:User {country: 'NL'})
RETURN a.handle, b.handle// Equivalent, easier to read for larger filters
MATCH (a:User)-[:FOLLOWS]->(b:User)
WHERE a.country = 'NL' AND b.country = 'NL'
RETURN a.handle, b.handleSee also
- CREATE — writing nodes and relationships.
- WHERE — predicate filtering.
- RETURN / WITH — projecting and piping rows.
- Paths — variable-length and shortest paths.
- Aggregation — grouping after
MATCH. - Concepts → Nodes, Relationships, Properties.