Skip to main content

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 nodeMATCH (n)
Match every node with a labelMATCH (n:Person)
Match by label + propertiesMATCH (n:Person {name: 'Ada'})
Match a relationshipMATCH (a)-[:KNOWS]->(b)
Match any type, any directionMATCH (a)-[r]-(b)
Left-join shapeOPTIONAL MATCH
Variable-length hopsMATCH (a)-[:R*1..3]->(b)
Bind the whole pathMATCH p = (a)-[:R]->(b)

Node patterns

Start with the simplest shape — a single node.

MATCH (n) RETURN n

One 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 labels

Multiple 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 n

Inline 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 b

The 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, b

Direction 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_away

Intermediate nodes can still be labelled and filtered:

MATCH (p:Person)-[:WORKS_AT]->(c:Company)-[:IN]->(:City {name: 'Amsterdam'})
RETURN p.name, c.name

For 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, b

For that same shape connected by a relationship, prefer:

MATCH (a:User)-[:FOLLOWS]->(b:User) RETURN a, b

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

Users 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 DESC

count(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_issues

Each 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,
length(p) AS hops,
nodes(p) AS via,
relationships(p) AS rels

length(p) is the hop count; nodes(p) and relationships(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, other

One row per FOLLOWS edge. Returns whole nodes.

2. Project properties

MATCH (u:User)-[:FOLLOWS]->(other:User)
RETURN u.handle AS follower, other.handle AS leader

Cleaner 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.handle

Same-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 50

5. 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 10

Each 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 1

LoraDB has no uniqueness constraints (see Limitations), so 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 20

Two-sided match

MATCH (src:User {id: $from}), (dst:User {id: $to})
MATCH (src)-[:FOLLOWS*1..3]->(dst)
RETURN src, dst

Useful with shortest paths.

MATCH (p:Person {id: $id})-[:WORKS_AT]->(c:Company)
RETURN p.name, collect(c.name) AS companies

Relationship existence check

Use EXISTS { pattern } to filter without an extra row:

MATCH (u:User)
WHERE EXISTS { (u)-[:FOLLOWS]->() }
RETURN u

Edge 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 a

Duplicate 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, c

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

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

Relationships 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, c

See 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.handle

See also