Chapter 03 · Cypher Fundamentals: MATCH, RETURN, WHERE, Parameters, Ordering, and Pagination

Read Graph Patterns with MATCH and Understand Variable Binding Across Nodes and Relationships

Treat MATCH as a row-producing pattern operation: bind nodes and relationships, expose fan-out, and make cardinality visible before adding filters or projections.

Beginner → Intermediate105–125 minutesMATCH binding/cardinality labNeo4j 2026.07.1 Community · Cypher 25Last reviewed: September 2026

Learning outcomes

AtlasMart needs a read API that answers “which products did a customer buy?” without treating Cypher as SQL with arrows pasted into the FROM clause. The key mental model is a stream of rows: MATCH finds graph patterns and binds variables in each successful match; later clauses transform or project those bindings.

01

Explain MATCH as pattern matching that produces rows of variable bindings.

02

Distinguish node variables, relationship variables, anonymous pattern elements, labels and relationship types.

03

Predict when one starting node expands to multiple rows and why duplicate-looking result values can be correct.

04

Trace variable scope through one query part and use EXPLAIN as structural evidence without executing the query.

05

Recognize when an undirected match changes the business meaning or cardinality of a read.

Chapter 03 continuity contract

Continue Chapters 01–02 with Neo4j Community 2026.07.1, database neo4j, explicit CYPHER 25 in version-sensitive examples, local container atlasmart-neo4j, Bolt 127.0.0.1:7687, HTTP 127.0.0.1:7474, and the existing constraint-backed AtlasMart domain IDs. This chapter does not redesign the graph; it adds a deterministic read fixture and treats Cypher as a pipeline of row bindings produced and transformed by patterns, predicates and projections.

Version and execution note

The current Cypher Manual covers Cypher 25. Cypher 5 is frozen while new language features since Neo4j 2025.06 are added to Cypher 25; current 2026.02+ newly created databases explicitly default to Cypher 25, while existing deployments may differ. The mandatory examples therefore use CYPHER 25 and avoid assuming a server-wide default. This environment has no running Neo4j/Docker runtime, so expected results are deterministic fixture invariants rather than fabricated captured output.

1. MATCH binds variables to successful graph patterns

In MATCH (c:Customer)-[r:PLACED]->(o:Order), c, r, and o are variables. Each row is one binding combination that satisfies the whole pattern. If Ava placed two orders, the same customer node can appear in two rows because the relationship/order bindings differ. The row count therefore follows matching paths, not the number of distinct starting nodes.

Pattern fragment Meaning Cardinality consequence
(c:Customer) Bind a node with label Customer One row per matched customer before later expansion.
-[r:PLACED]-> Bind an outgoing PLACED relationship One input row can become many rows if the customer has many PLACED relationships.
(o:Order) Bind the relationship target The output row carries both customer and order bindings.
(c)-[:PLACED]->(o) Anonymous relationship Same structural match, but no relationship variable is carried forward.
(c)-[:PLACED]-(o) Ignore stored direction during matching Can match either orientation; useful only if business semantics justify it.

2. Build a deterministic fixture and count bindings

Cypher · idempotent AtlasMart read fixture
CYPHER 25CREATE CONSTRAINT customer_id IF NOT EXISTS FOR (c:Customer) REQUIRE c.customerId IS UNIQUE;CREATE CONSTRAINT product_id IF NOT EXISTS FOR (p:Product) REQUIRE p.productId IS UNIQUE;CREATE CONSTRAINT order_id IF NOT EXISTS FOR (o:Order) REQUIRE o.orderId IS UNIQUE;CREATE CONSTRAINT category_id IF NOT EXISTS FOR (c:Category) REQUIRE c.categoryId IS UNIQUE;MERGE (c1:Customer {customerId:'C-1001'}) SET c1.name='Ava Chen', c1.tier='gold', c1.region='eu'MERGE (c2:Customer {customerId:'C-1002'}) SET c2.name='Noah Smith', c2.tier='silver', c2.region='us'MERGE (c3:Customer {customerId:'C-1003'}) SET c3.name='Mina Rahimi', c3.tier='gold', c3.region='me'MERGE (c4:Customer {customerId:'C-1004'}) SET c4.name='Leo Martin', c4.region='eu'MERGE (cat1:Category {categoryId:'CAT-CAMERAS'}) SET cat1.name='Cameras'MERGE (cat2:Category {categoryId:'CAT-AUDIO'}) SET cat2.name='Audio'MERGE (p1:Product {productId:'P-1001'}) SET p1.name='Trail Camera', p1.price=129.90, p1.rating=4.7MERGE (p2:Product {productId:'P-1002'}) SET p2.name='Studio Headphones', p2.price=89.00, p2.rating=4.7MERGE (p3:Product {productId:'P-1003'}) SET p3.name='Action Camera', p3.price=219.00, p3.rating=4.5MERGE (p4:Product {productId:'P-1004'}) SET p4.name='USB Microphone', p4.price=75.00MERGE (p1)-[:IN_CATEGORY]->(cat1)MERGE (p3)-[:IN_CATEGORY]->(cat1)MERGE (p2)-[:IN_CATEGORY]->(cat2)MERGE (p4)-[:IN_CATEGORY]->(cat2)MERGE (o1:Order {orderId:'O-2001'}) SET o1.placedAt=datetime('2026-08-01T09:00:00Z'), o1.status='paid', o1.total=218.90MERGE (o2:Order {orderId:'O-2002'}) SET o2.placedAt=datetime('2026-08-02T10:30:00Z'), o2.status='paid', o2.total=219.00MERGE (o3:Order {orderId:'O-2003'}) SET o3.placedAt=datetime('2026-08-03T12:00:00Z'), o3.status='shipped', o3.total=129.90MERGE (o4:Order {orderId:'O-2004'}) SET o4.placedAt=datetime('2026-08-04T14:15:00Z'), o4.status='paid', o4.total=164.00MERGE (o5:Order {orderId:'O-2005'}) SET o5.placedAt=datetime('2026-08-04T14:15:00Z'), o5.status='paid', o5.total=75.00MERGE (c1)-[:PLACED]->(o1)MERGE (c2)-[:PLACED]->(o2)MERGE (c1)-[:PLACED]->(o3)MERGE (c3)-[:PLACED]->(o4)MERGE (c4)-[:PLACED]->(o5)MERGE (o1)-[:CONTAINS {quantity:1}]->(p1)MERGE (o1)-[:CONTAINS {quantity:1}]->(p2)MERGE (o2)-[:CONTAINS {quantity:1}]->(p3)MERGE (o3)-[:CONTAINS {quantity:1}]->(p1)MERGE (o4)-[:CONTAINS {quantity:1}]->(p2)MERGE (o4)-[:CONTAINS {quantity:1}]->(p4)MERGE (o5)-[:CONTAINS {quantity:1}]->(p4);
Cypher · make row multiplication visible
CYPHER 25MATCH (c:Customer {customerId:$customerId})-[r:PLACED]->(o:Order)RETURN c.customerId AS customerId,       type(r) AS relationshipType,       o.orderId AS orderIdORDER BY o.orderId;

With $customerId='C-1001', the fixture should yield exactly two rows: O-2001 and O-2003. That proves two matching PLACED relationships exist for Ava; it does not mean the Customer node was duplicated.

Cypher · expansion through order lines
CYPHER 25MATCH (c:Customer {customerId:$customerId})-[:PLACED]->(o:Order)-[:CONTAINS]->(p:Product)RETURN c.customerId, o.orderId, p.productIdORDER BY o.orderId, p.productId;

For Ava, the first order has two CONTAINS relationships and the second has one, so the pattern yields three rows. Thinking in rows makes later DISTINCT, aggregation, pagination and accidental fan-out understandable.

3. EXPLAIN proves planned operators, not returned data

Cypher · inspect a read without executing it
CYPHER 25 EXPLAINMATCH (c:Customer {customerId:$customerId})-[:PLACED]->(o:Order)RETURN o.orderIdORDER BY o.orderId;

EXPLAIN asks Neo4j to plan without executing. Operator names and estimated rows are release/runtime dependent, so the lesson uses them as evidence of access-path and expansion shape rather than as constants to memorize. Later Chapter 10 will use PROFILE, which does execute and collects actual runtime counters.

4. Wrong approach: assume matching a customer returns one row

A developer writes a query that starts from one customer and returns c.name after expanding orders/products, then is surprised that “Ava Chen” repeats. The repetition is not a duplicate Customer record; it is one value projected from multiple binding rows. Fix the requirement, not the symptom: if the API wants unique customers, project DISTINCT c; if it wants orders or products, keep the expanded rows or aggregate intentionally.

Cypher · prove DISTINCT changes rows, not stored graph
CYPHER 25MATCH (c:Customer)-[:PLACED]->(:Order)-[:CONTAINS]->(:Product)RETURN c.customerId AS customerIdORDER BY customerId;CYPHER 25MATCH (c:Customer)-[:PLACED]->(:Order)-[:CONTAINS]->(:Product)RETURN DISTINCT c.customerId AS customerIdORDER BY customerId;

The second query removes duplicate projected rows; it does not delete, merge, or otherwise alter graph entities.

Lab: build the first read-query contract

Cypher Shell · parameterized customer purchase read
:param {customerId: 'C-1001'};CYPHER 25MATCH (c:Customer {customerId:$customerId})-[:PLACED]->(o:Order)-[line:CONTAINS]->(p:Product)RETURN o.orderId, o.placedAt, p.productId, p.name, line.quantityORDER BY o.placedAt, o.orderId, p.productId;

Verification checklist: the parameter is bound separately from query text; the row count is three for C-1001; every row has one order/product binding; order direction is Customer→Order; the sort uses explicit stable keys; and changing the parameter to an unknown customer returns zero rows rather than an error.

Check your understanding

  1. What does a Cypher variable represent during MATCH?
  2. Why can one Customer binding produce several rows?
  3. Does RETURN DISTINCT merge duplicate nodes in storage?
  4. What does EXPLAIN not do?
  5. When is an undirected relationship pattern risky?
Review the answers

1. A variable names a value—often a node or relationship—bound in each successful row of the pattern/query part.

2. Expansion can match multiple relationships/targets, producing one row per successful binding combination.

3. No. DISTINCT removes duplicate projected result rows only.

4. It does not execute the query or produce runtime row/database-hit measurements.

5. When relationship orientation carries domain meaning; ignoring direction can match unintended structures and change cardinality.

Production judgment

Row cardinality is both a correctness surface and a cost surface. A query that accidentally expands from Customer→Order→Product before applying the intended business restriction may return correct-looking final columns while doing far more work than expected. Treat each pattern hop as a possible multiplier, keep domain direction explicit, and make API result cardinality part of tests. Next, we place predicates correctly and make null/parameter semantics observable.

Summary and next step

Read Graph Patterns with MATCH and Understand Variable Binding Across Nodes and Relationships is useful only when its assumptions and observed evidence stay attached to the decision. The examples above establish a reproducible mechanism and boundary; they do not turn one lab result into a universal production rule.

Next, continue to Filter with WHERE, Predicates, Labels, Types, Property Access, Null Semantics, and Parameters. Carry forward the verified assumptions, fixture state, version/edition boundaries, and measurements from this lesson instead of treating the next topic as an isolated recipe.

Authoritative references

  • Current Neo4j versions — Official current-release and 5.26 LTS patch snapshot.
  • Cypher Manual introduction — Current Cypher 25 baseline and Cypher 5 compatibility framing.
  • MATCH — Pattern matching and variable-binding semantics.
  • Variables — Variable naming and scope across query parts.
  • WHERE — Pattern constraints and post-WITH filtering semantics.
  • Predicates — Boolean/comparison/string/list/type predicates and three-valued results.
  • Working with null — Null propagation and missing-property semantics.
  • RETURN — Projection, aliases, expressions and DISTINCT semantics.
  • List expressions and pattern comprehension — Current fixed/variable pattern-comprehension syntax and behavior.
  • ORDER BY — Only ORDER BY guarantees result ordering; sort semantics and index-backed ordering.
  • SKIP / OFFSET — Offset semantics and OFFSET synonym.
  • LIMIT — Row limiting semantics and the absence of ordering guarantees without ORDER BY.
  • Cypher Shell — Bolt CLI, parameter support, script/file execution and output modes.
  • Query tuning and plans — EXPLAIN/PROFILE and execution-plan evidence.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.