Chapter 04 · Advanced Pattern Matching: Variable Length, OPTIONAL MATCH, Paths, Quantified Patterns, and Shortest Paths
Diagnose a Path Explosion and Rewrite the Query with Better Anchors, Bounds, and Predicates
Reproduce combinatorial growth safely, then reduce the search space by changing the query contract and traversal shape before reaching for arbitrary tuning values.
Learning outcomes
The final lesson turns a common incident into a repeatable diagnostic: an innocent-looking multi-hop query becomes slow as graph degree grows. AtlasMart will reproduce the growth safely on a tiny cyclic graph, then reduce work by changing the question and query shape rather than by guessing a universal timeout.
Measure path-count growth as bounds increase on a deterministic cyclic fixture.
Identify broad starts, permissive relationship directions/types and late predicates as cardinality multipliers.
Rewrite path enumeration with selective anchors, finite bounds and inline predicates.
Use EXISTS or a shortest-path selector when the application does not need every path.
Create a production checklist covering degree distributions, timeouts, plan evidence, memory and regression tests.
Continue Chapters 01–03 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 constraint-backed AtlasMart domain identifiers. Chapter 04
adds a small synthetic operational handoff subgraph on
existing Supplier/Category/Store concepts; it is intentionally
isolated for path-mechanics exercises and does not replace the
transactional relationships modeled earlier.
Neo4j 2026.07.1 is the current 2026 release used
by this course snapshot; 5.26.30 remains the
current 5.26 LTS comparison line. The current manual covers
Cypher 25; Cypher 5 is frozen. Quantified path
patterns/relationships date from Neo4j 5.9, while explicit
Cypher 25 path modes such as ACYCLIC arrived
later and have version-sensitive combination rules. Commands
here were checked against current documentation but could not
be executed in this generation environment, so expected output
is described by deterministic invariants rather than
fabricated captures.
1. Reproduce the symptom safely
CYPHER 25CREATE CONSTRAINT supplier_id IF NOT EXISTS FOR (s:Supplier) REQUIRE s.supplierId IS UNIQUE;CREATE CONSTRAINT category_id IF NOT EXISTS FOR (c:Category) REQUIRE c.categoryId IS UNIQUE;CREATE CONSTRAINT store_id IF NOT EXISTS FOR (s:Store) REQUIRE s.storeId IS UNIQUE;CREATE CONSTRAINT ops_point_id IF NOT EXISTS FOR (n:OpsPoint) REQUIRE n.pointId IS UNIQUE;MERGE (s1:Supplier {supplierId:'SUP-3001'}) SET s1:OpsPoint, s1.pointId='OP-SUP-1', s1.name='Northwind Optics';MERGE (s2:Supplier {supplierId:'SUP-3002'}) SET s2:OpsPoint, s2.pointId='OP-SUP-2', s2.name='Audio Forge';MERGE (c1:Category {categoryId:'CAT-CAMERAS'}) SET c1:OpsPoint, c1.pointId='OP-CAT-CAM', c1.name='Cameras';MERGE (c2:Category {categoryId:'CAT-AUDIO'}) SET c2:OpsPoint, c2.pointId='OP-CAT-AUD', c2.name='Audio';MERGE (st1:Store {storeId:'ST-001'}) SET st1:OpsPoint, st1.pointId='OP-ST-1', st1.name='Central', st1.region='west';MERGE (st2:Store {storeId:'ST-002'}) SET st2:OpsPoint, st2.pointId='OP-ST-2', st2.name='Harbor', st2.region='east';MERGE (st3:Store {storeId:'ST-003'}) SET st3:OpsPoint, st3.pointId='OP-ST-3', st3.name='Airport', st3.region='north';MATCH (s1:OpsPoint {pointId:'OP-SUP-1'}), (s2:OpsPoint {pointId:'OP-SUP-2'}), (c1:OpsPoint {pointId:'OP-CAT-CAM'}), (c2:OpsPoint {pointId:'OP-CAT-AUD'}), (st1:OpsPoint {pointId:'OP-ST-1'}), (st2:OpsPoint {pointId:'OP-ST-2'}), (st3:OpsPoint {pointId:'OP-ST-3'})MERGE (s1)-[:HANDOFF_TO {routeId:'R01', minutes:30, active:true}]->(st1)MERGE (s1)-[:HANDOFF_TO {routeId:'R02', minutes:10, active:true}]->(c1)MERGE (c1)-[:HANDOFF_TO {routeId:'R03', minutes:8, active:true}]->(st1)MERGE (c1)-[:HANDOFF_TO {routeId:'R04', minutes:5, active:true}]->(c2)MERGE (st1)-[:HANDOFF_TO {routeId:'R05', minutes:12, active:true}]->(s2)MERGE (st1)-[:HANDOFF_TO {routeId:'R06', minutes:11, active:false}]->(c2)MERGE (s2)-[:HANDOFF_TO {routeId:'R07', minutes:9, active:true}]->(c2)MERGE (s2)-[:HANDOFF_TO {routeId:'R08', minutes:6, active:true}]->(st2)MERGE (c2)-[:HANDOFF_TO {routeId:'R09', minutes:7, active:true}]->(st2)MERGE (st2)-[:HANDOFF_TO {routeId:'R10', minutes:14, active:true}]->(s1)MERGE (st2)-[:HANDOFF_TO {routeId:'R11', minutes:4, active:true}]->(st3)MERGE (st3)-[:HANDOFF_TO {routeId:'R12', minutes:13, active:true}]->(c1);
CYPHER 25 PROFILEMATCH p=(:OpsPoint {pointId:'OP-SUP-1'})-[:HANDOFF_TO]->{1,6}(:OpsPoint)RETURN count(p) AS paths;
On this tiny graph, a six-hop bound is safe enough for a learning exercise. Record actual rows, db hits and elapsed time from your own runtime. The point is not the absolute number; it is how rapidly candidates increase as the bound and branching factor grow.
CYPHER 25 PROFILEMATCH p=(:OpsPoint {pointId:'OP-SUP-1'})-[:HANDOFF_TO]->{1,3}(:OpsPoint)RETURN count(p) AS paths;
2. Diagnose the multipliers before tuning
| Multiplier | Diagnostic question | Typical repair |
|---|---|---|
| Broad start | Did the query anchor one constrained domain ID or scan many starts? | Start from a unique/indexed identifier when the request names one entity. |
| Relationship scope | Are type and direction actually part of the domain contract? | Specify them; do not use undirected generic expansion casually. |
| Depth | What is the maximum business-meaningful number of hops? | Encode a finite upper bound. |
| Late filtering | Can invalid edges/nodes be rejected during traversal? | Move safe predicates into the quantified fragment. |
| Wrong result requirement | Does the caller need every path? | Use EXISTS, shortest selector, aggregation or a precomputed/GDS approach instead. |
| Dense/cyclic data | What are p50/p95/max degree and cycle patterns? | Test with realistic degree distributions, not only node count. |
3. Rewrite 1: selective anchors, target and inline edge predicate
CYPHER 25MATCH (source:Supplier:OpsPoint {supplierId:$supplierId}), (target:Store:OpsPoint {storeId:$storeId})MATCH p=(source) ((a:OpsPoint)-[r:HANDOFF_TO]->(b:OpsPoint) WHERE r.active=true){1,4} (target)RETURN length(p) AS hops, [r IN relationships(p) | r.routeId] AS routesORDER BY hops, routes;
The query now expresses a much smaller question. The unique source/target anchors bound endpoint cardinality; relationship direction/type encode semantics; the maximum depth encodes an API limit; the inline predicate prunes inactive branches as traversal proceeds.
4. Rewrite 2: if the caller only needs existence, do not enumerate every path
CYPHER 25MATCH (source:Supplier:OpsPoint {supplierId:$supplierId})RETURN EXISTS { MATCH (source)-[:HANDOFF_TO]->{1,4}(:Store:OpsPoint {storeId:$storeId})} AS reachable;
An existence question can stop being semantically obligated to return every path. The planner/runtime remains version/data dependent, so measure it, but the query contract is now aligned with the caller’s actual need.
CYPHER 25MATCH (source:Supplier:OpsPoint {supplierId:$supplierId}), (target:Store:OpsPoint {storeId:$storeId})MATCH p=ANY SHORTEST (source)-[:HANDOFF_TO]->{1,8}(target)RETURN [n IN nodes(p) | n.pointId] AS points, length(p) AS hops;
5. Failure-injection acceptance test
Add temporary branching edges only inside the disposable Chapter 04 fixture, measure the broad query, then remove those exact edges and repeat the bounded rewrite. Never create high-degree synthetic edges in a shared or production database.
CYPHER 25MATCH (s:OpsPoint {pointId:'OP-SUP-1'}), (a:OpsPoint {pointId:'OP-ST-3'}), (b:OpsPoint {pointId:'OP-CAT-AUD'})MERGE (s)-[:HANDOFF_TO {routeId:'CHAOS-1', minutes:99, active:true}]->(a)MERGE (a)-[:HANDOFF_TO {routeId:'CHAOS-2', minutes:99, active:true}]->(b);// ...run only the bounded diagnostic queries from this lesson...MATCH ()-[r:HANDOFF_TO]->() WHERE r.routeId STARTS WITH 'CHAOS-' DELETE r;
Verification checklist: confirm the CHAOS relationships are gone; record path counts before/after; retain the exact query text, parameters, server/Cypher version, fixture edge count and PROFILE summary. A performance claim without those inputs is not reproducible.
CYPHER 25MATCH ()-[r:HANDOFF_TO]->() DELETE r;MATCH (n:OpsPoint) REMOVE n:OpsPoint, n.pointId;DROP CONSTRAINT ops_point_id IF EXISTS;
The reset removes only Chapter 04 HANDOFF_TO edges
and the temporary OpsPoint label/property. It
intentionally leaves core Supplier/Category/Store nodes and
their earlier-course properties intact.
Check your understanding
- Why is node count alone a poor predictor of path-query cost?
- What three query-shape controls usually matter before low-level tuning?
- When should EXISTS replace path enumeration?
- Why is a finite bound still required in the lab even with relationship uniqueness?
- What evidence should accompany a path-performance claim?
Review the answers
1. Degree/fan-out, depth, cycles and predicates determine the number of candidate paths.
2. Selective anchors/targets, relationship type/direction, and a business-meaningful finite depth (plus early predicates).
3. When the API asks only whether a qualifying path exists, not for every path value.
4. The finite search space can still be combinatorially huge; uniqueness prevents one kind of repetition but not branching explosion.
5. Exact query/parameters, graph size and degree/edge fixture, server/Cypher version, plan/profile rows/db hits/memory, hardware/container resources and timing methodology.
Summary and next step
Chapter 04 makes multi-hop Cypher bounded and observable. Paths are values; optional patterns preserve rows; cycles do not imply repeated relationships under default matching; shortest by hops is not weighted shortest; and path explosion is solved first by narrowing the question. Chapter 05 now builds on this row/path mental model with expressions, aggregation, UNWIND, collections, maps and subqueries.
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.
- Patterns — Current graph/path matching overview, including shortest paths and match/path modes.
- Variable-length paths — Quantified path patterns, quantified relationships, group variables and inline predicates.
- Variable-length path reference — Formal syntax and rules for quantified and legacy variable-length patterns.
- Path-pattern reference — Path values, path-pattern composition and matching rules.
- Unique relationship paths — Default relationship-uniqueness behavior and DIFFERENT RELATIONSHIPS semantics.
- Match modes and path modes — Current Cypher 25 WALK/TRAIL/ACYCLIC and match-mode compatibility rules.
- OPTIONAL MATCH — Outer-row preservation and null introduction when a pattern is absent.
- WHERE — WHERE as a subclause of MATCH/OPTIONAL MATCH and its pattern-scoping consequences.
- Path functions — nodes(), relationships(), length()/path_length() and path-related list/predicate functions.
- Shortest paths — Current SHORTEST/ALL SHORTEST path selector semantics.
- Query plans and operators — Execution-plan operators and row/db-hit/memory evidence.
- GDS graph algorithms — Graph Data Science algorithm families including path finding.
- GDS Dijkstra source-target — Weighted positive-edge shortest-path algorithm for projected GDS graphs.