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.

Intermediate → Advanced130–155 minutesPath-explosion diagnosis labNeo4j 2026.07.1 Community · Cypher 25Last reviewed: September 2026

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.

01

Measure path-count growth as bounds increase on a deterministic cyclic fixture.

02

Identify broad starts, permissive relationship directions/types and late predicates as cardinality multipliers.

03

Rewrite path enumeration with selective anchors, finite bounds and inline predicates.

04

Use EXISTS or a shortest-path selector when the application does not need every path.

05

Create a production checklist covering degree distributions, timeouts, plan evidence, memory and regression tests.

Chapter 04 continuity contract

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.

Version and execution note

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 · idempotent cyclic AtlasMart handoff fixture
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 · bounded path-explosion probe
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 · compare smaller bound under the same fixture
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 · bounded active route to one target
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 · boolean reachability contract
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 · if one nearest route is enough
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 · disposable synthetic branches
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 · remove only Chapter 04 lab edges/labels
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

  1. Why is node count alone a poor predictor of path-query cost?
  2. What three query-shape controls usually matter before low-level tuning?
  3. When should EXISTS replace path enumeration?
  4. Why is a finite bound still required in the lab even with relationship uniqueness?
  5. 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

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.