Chapter 10 · Query Planning and Profiling: Cardinality, Operators, Index Selection, and Plan Stability

Tune a Slow Cypher Query by Changing Model, Predicate, Index, or Query Shape—Then Re-Measure

Run a controlled AtlasMart tuning experiment that separates query, predicate, index and model changes and accepts only evidence-backed improvements.

Advanced165–210 minutesEnd-to-end tuning + regression labNeo4j 2026.07.1 Community · Cypher 25Last reviewed: September 2026

Learning outcomes

The tuning exercise now combines everything: one AtlasMart traversal, one baseline, one controlled change at a time, and an explicit decision about whether the right fix is query shape, predicate placement, index support, model shape—or no change at all.

01

Create a baseline that captures plan shape, actual work, result correctness and a latency distribution.

02

Change exactly one tuning dimension at a time and state the causal hypothesis before running it.

03

Compare customer-first and product-first query shapes under both dense and typical parameters.

04

Detect when an added index is redundant because the traversal binds the indexed entity before filtering it.

05

Produce an evidence-backed acceptance/rollback decision instead of declaring victory from one faster run.

Chapter 10 baseline · reviewed 9 September 2026

The mandatory lab continues Neo4j Community 2026.07.1, database neo4j, explicit CYPHER 25 for version-sensitive examples, authentication enabled, no mandatory APOC/GDS plugin, and the AtlasMart identifiers/model established in Chapters 01–09. Neo4j 5.26.30 remains the LTS comparison line. Community uses the slotted runtime by default. Enterprise uses the pipelined runtime by default; Aura uses pipelined, and the parallel runtime is an Enterprise capability for supported read workloads. Therefore plan columns such as pipeline timing and page-cache hits/misses are not universal Community output.

Evidence and measurement note

This generation environment does not run Neo4j or Docker. Commands were checked against current official documentation but were not executed here. The lessons never invent DB-hit counts, memory figures, operator timings or latency improvements. Instead, they define deterministic fixture invariants and tell you exactly which values to record from your own EXPLAIN/PROFILE runs. All disposable Chapter 10 entities use labTag='ch10'; disposable schema objects use ch10_*.

1. State the workload contract before touching the query

Contract field Chapter 10 lab value
Question Which high-priced products in a category were bought by a specified customer?
Correctness Same product set for all semantically equivalent rewrites
Dense case CH10-C-0001 (800 orders in fixture)
Typical case CH10-C-0050 (roughly a small share of remaining 1200 orders)
Product predicate categoryCode + price threshold
Schema customer/order/product uniqueness + ch10_product_category_price range index
Measurements Plan, estimates/actual rows, DB hits, memory when shown, result rows, latency distribution
No fabricated target You choose an application SLO; this lesson provides method, not universal milliseconds

2. Baseline: customer-first traversal

Cypher · baseline query
CYPHER 25PROFILEMATCH (c:Customer {customerId:$customerId})-[:PLACED]->(o:Order)-[line:CONTAINS]->(p:Product)WHERE p.categoryCode=$category AND p.price >= $minPriceRETURN DISTINCT p.productId,p.name,p.priceORDER BY p.price DESC,p.productId;

Hypothesis: the unique customer seek is cheap, but the hot customer generates many order and line rows before the product predicate can reject them. Record the actual rows at each expansion and filter. If that hypothesis is wrong, do not proceed as if it were true.

3. Change A: make product selectivity an alternative starting point

The composite product index from Chapter 09/this lab can only help as a starting access path if the query shape allows the planner to bind products through the indexed predicate before they are already reached by traversal. Compare a product-first rewrite, preserving result semantics.

Cypher · product-first alternative
CYPHER 25PROFILEMATCH (p:Product)WHERE p.categoryCode=$category AND p.price >= $minPriceMATCH (p)<-[line:CONTAINS]-(o:Order)<-[:PLACED]-(c:Customer {customerId:$customerId})RETURN DISTINCT p.productId,p.name,p.priceORDER BY p.price DESC,p.productId;

For a highly selective product predicate, the indexed start may reduce candidates. For a broad predicate, traversing backward from many products may be worse than customer-first. Test both dense and typical customers plus more than one price threshold.

4. Change B: add a tempting but possibly useless index

A common mistake is to create an index on every filtered property without checking whether the plan can use it. Create a disposable single-property price index, capture EXPLAIN, and ask whether it changes the chosen access path compared with the existing composite schema.

Cypher · controlled redundant-index experiment
CYPHER 25CREATE RANGE INDEX ch10_product_price_only IF NOT EXISTSFOR (p:Product) ON (p.price);CALL db.awaitIndex('ch10_product_price_only',300);EXPLAINMATCH (p:Product)WHERE p.categoryCode=$category AND p.price >= $minPriceMATCH (p)<-[:CONTAINS]-(o:Order)<-[:PLACED]-(c:Customer {customerId:$customerId})RETURN DISTINCT p.productId,p.price;DROP INDEX ch10_product_price_only IF EXISTS;

If the planner keeps choosing the composite index or another better path, the added index has not earned its ongoing write/storage/build cost for this workload. Preserve the plan evidence with the schema decision.

5. Change C: model-level mitigation for repeated hot-customer analytics

If the actual requirement repeatedly asks for aggregated history across a high-degree customer, a query rewrite may not be enough. A separate summary/fact structure (for example, customer-category purchase aggregates by time window) can reduce read work but adds write complexity, freshness semantics and reconciliation. This chapter does not silently introduce that model. It requires you to prove the read bottleneck first, define the summary’s consistency contract, then benchmark both read and write consequences.

Candidate change Potential benefit New cost/risk
Query anchor rewrite No data-model migration May trade one broad side for another
Additional index Cheaper eligible predicate lookup Write/storage/build overhead; may be unused
Predicate/window narrowing Less fan-out/result work Changes API semantics unless requirement permits it
Precomputed summary/fact node Fast repeated aggregate reads Write amplification, freshness and reconciliation
Relationship/model refactor Can bound traversals semantically Migration complexity and changed write/query contracts

6. Re-measure with a causal experiment sheet

Record every variant · do not fill with invented numbers
variant,case,parameterShape,planStart,estimatedRowsAtHotspot,actualRowsAtHotspot,dbHits,peakMemoryBytes,p50Ms,p95Ms,p99Ms,resultRows,correctResult,decisionbaseline,hot,dense,,,,,,,,,,,baseline,typical,sparse,,,,,,,,,,,product-first,hot,dense,,,,,,,,,,,product-first,typical,sparse,,,,,,,,,,,price-index-test,hot,dense,,,,,,,,,,,

Warm up according to a declared protocol, run enough repetitions to report a distribution rather than a single average, and keep correctness checks alongside performance. If the “faster” version returns different products or handles nulls differently, it is not a tuning win.

7. Deliberately wrong: change index, query, runtime and model together

Multiple simultaneous changes destroy causal attribution. If the result improves, you cannot know which change mattered; if it regresses, rollback becomes ambiguous. The repair is a sequence of reversible experiments with one independent variable at a time and a stable workload fixture.

Check your understanding

  1. Why can an index on Product.price be useless in a customer-first traversal?
  2. Why test both hot and typical customers?
  3. What must remain invariant across tuning rewrites?
  4. When is a model-level summary justified?
  5. Why change one dimension at a time?
Review the answers

1. Because Product may already be bound by traversal before the price predicate is evaluated; an index helps most when it can serve an eligible access path.

2. Because degree skew changes the number of rows produced after the same unique starting seek.

3. Business semantics and result correctness, including ordering/pagination/null behavior where part of the API contract.

4. After evidence shows recurring graph-shape work that a summary can reduce and the added write/freshness/reconciliation cost is explicitly acceptable.

5. So measured differences can be causally attributed and safely rolled back.

8. Cleanup and Chapter 11 bridge

Keep the fixture if you want to reuse it for transaction/locking experiments, or remove only Chapter 10 objects with the isolated cleanup below. Chapter 11 moves from plan work to transaction behavior: isolation, locks, deadlocks, retries and consistency.

Cypher · optional isolated Chapter 10 cleanup
CYPHER 25MATCH (n) WHERE n.labTag='ch10' DETACH DELETE n;DROP INDEX ch10_product_category_price IF EXISTS;DROP INDEX ch10_order_status IF EXISTS;DROP INDEX ch10_product_price_only IF EXISTS;DROP INDEX ch10_customer_segment IF EXISTS;

Summary

Performance engineering is a controlled scientific loop: representative workload → plan/runtime evidence → one hypothesis → one change → re-measure → correctness check → accept or rollback. That discipline is more durable than memorizing “good” operator names or universal index recipes.

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.