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

Project Results with RETURN, Aliases, DISTINCT, Expressions, Maps, and Pattern Comprehensions

Turn binding rows into deliberate API shapes and use DISTINCT only when uniqueness is part of the result semantics.

Beginner → Intermediate105–125 minutesProjection/result-shape labNeo4j 2026.07.1 Community · Cypher 25Last reviewed: September 2026

Learning outcomes

A graph read is not finished when a pattern matches. AtlasMart must turn binding rows into API values: scalar properties, aliases, expressions, maps, unique rows, and compact related-data lists. RETURN defines that projection boundary.

01

Project nodes, relationships, scalar properties and expressions with stable aliases.

02

Explain DISTINCT as result-row deduplication over selected expressions.

03

Build map projections for API-shaped results without confusing query maps with stored properties.

04

Use pattern comprehensions to produce related-value lists while tracking cardinality and null behavior.

05

Recognize when a projection hides accidental expansion instead of fixing the query shape.

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. RETURN defines the result contract

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 · project a stable order API row
CYPHER 25MATCH (c:Customer)-[:PLACED]->(o:Order)WHERE o.orderId=$orderIdRETURN o.orderId AS orderId,       c.customerId AS customerId,       c.name AS customerName,       o.status AS status,       o.total AS total;

Returning an entire node is convenient during exploration, but production APIs usually benefit from explicit fields and aliases. Explicit projection prevents incidental property additions from silently changing an API response.

2. DISTINCT deduplicates selected result rows

Cypher · duplicate product values created by path multiplicity
CYPHER 25MATCH (:Customer {customerId:'C-1001'})-[:PLACED]->(:Order)-[:CONTAINS]->(p:Product)RETURN p.productId AS productIdORDER BY productId;CYPHER 25MATCH (:Customer {customerId:'C-1001'})-[:PLACED]->(:Order)-[:CONTAINS]->(p:Product)RETURN DISTINCT p.productId AS productIdORDER BY productId;

Trail Camera appears through two different orders. The first query correctly has two Trail Camera rows because there are two purchase paths. The second says the API wants the set of product IDs. DISTINCT is therefore a semantic choice. It can also require state/memory, so do not sprinkle it everywhere to hide unexplained row multiplication.

Cypher 25 note

Current Cypher also supports explicit RETURN ALL as GQL-aligned syntax; simple RETURN already keeps duplicates. For compatibility-focused examples the course uses ordinary RETURN and RETURN DISTINCT, which keep the intent obvious across Cypher 5/25.

3. Expressions and map projections shape application data

Cypher · map projection with derived fields
CYPHER 25MATCH (c:Customer)-[:PLACED]->(o:Order)WHERE o.orderId=$orderIdRETURN o {  .orderId,  .status,  .total,  customer: c {.customerId, .name},  isLarge: o.total >= $largeOrderThreshold} AS order;

The returned map is a constructed query value. It is ideal for driver/API transport but is not a nested property stored on the Order node. Its keys are part of the response contract and should be tested like any serializer.

4. Pattern comprehension can keep one outer row while collecting related values

Cypher · one order row with a related product list
CYPHER 25MATCH (o:Order {orderId:$orderId})RETURN o.orderId AS orderId,       [(o)-[line:CONTAINS]->(p:Product) |          p { .productId, .name, quantity: line.quantity }       ] AS items;

A pattern comprehension returns a list derived from matching a pattern. It can be more compact than returning one row per item, but it does not make fan-out free: Neo4j still has to match the relationships, and the resulting list occupies memory/driver payload. Bound the outer lookup and understand expected degree.

5. Wrong approach: DISTINCT as a universal repair tool

If an API unexpectedly emits many rows, adding DISTINCT may make the final output look right while masking a Cartesian product or incorrect expansion. Diagnose cardinality first. Return enough variables to reveal the binding rows, then decide whether duplicates are semantically identical or represent different paths/events.

Cypher · expose the rows before deciding to deduplicate
CYPHER 25MATCH (c:Customer {customerId:$customerId})-[:PLACED]->(o:Order)-[:CONTAINS]->(p:Product)RETURN c.customerId, o.orderId, p.productId, p.nameORDER BY o.orderId, p.productId;

Lab: build three stable response shapes

Cypher · scalar, map and distinct-set responses
CYPHER 25MATCH (o:Order)RETURN o.orderId, o.status, o.totalORDER BY o.orderId;CYPHER 25MATCH (c:Customer)-[:PLACED]->(o:Order)WHERE c.customerId=$customerIdRETURN o {.orderId,.placedAt,.status,.total} AS orderORDER BY o.placedAt, o.orderId;CYPHER 25MATCH (:Customer {customerId:$customerId})-[:PLACED]->(:Order)-[:CONTAINS]->(p:Product)RETURN DISTINCT p.productId, p.nameORDER BY p.productId;

Verification: each result shape has documented fields; aliases do not depend on internal IDs; the map projection returns one row per matched Order; the distinct-set query intentionally removes repeated purchase paths; and all public ordering is explicit.

Check your understanding

  1. What does RETURN control?
  2. What exactly does DISTINCT deduplicate?
  3. Does a map projection persist a nested map property?
  4. Why can pattern comprehension still be expensive?
  5. What should you inspect before adding DISTINCT to an unexpectedly large result?
Review the answers

1. The expressions/variables included in the result set and their aliases.

2. Duplicate result rows over the selected expressions, not stored graph entities.

3. No. It constructs a result value.

4. Its inner pattern still traverses relationships and materializes list values.

5. The underlying binding rows/cardinality and query pattern to identify the source of multiplication.

Production judgment

Projection is part of schema governance for an API. Prefer explicit fields, stable aliases and bounded related lists. Treat DISTINCT as semantics, not cleanup. The next lesson adds ordering and pagination, where apparently harmless omissions can produce unstable page boundaries and expensive deep offsets.

Summary and next step

Project Results with RETURN, Aliases, DISTINCT, Expressions, Maps, and Pattern Comprehensions 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 ORDER BY, SKIP/OFFSET, LIMIT, Deterministic Pagination, and Why Deep Pagination Can Be Expensive. 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.