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

EXPLAIN vs PROFILE, Logical/Physical Operators, Rows, Database Hits, Memory, and Pipeline Interpretation

Turn AtlasMart query plans into evidence rather than screenshots: understand what was estimated, what actually ran, and where work accumulated.

Advanced145–180 minutesEXPLAIN/PROFILE evidence labNeo4j 2026.07.1 Community · Cypher 25Last reviewed: September 2026

Learning outcomes

AtlasMart has a query that “sometimes feels slow,” but elapsed time alone cannot tell whether the problem is an expensive starting scan, fan-out after a good seek, a blocking sort, an aggregation buffer, a cache-state difference, or simply a larger result. This lesson turns the plan into the primary diagnostic artifact.

01

Distinguish planning from execution and explain why PROFILE is not a harmless EXPLAIN with more columns.

02

Read a query plan bottom-up as an operator tree and separate logical intent from physical runtime execution.

03

Interpret estimated rows, actual rows, database hits and memory without treating any single metric as latency.

04

Explain lazy pipelines, blocking/eager operators, and edition/runtime-dependent plan columns.

05

Capture a reproducible AtlasMart baseline before changing indexes, predicates, model or query shape.

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. EXPLAIN predicts; PROFILE executes

EXPLAIN compiles a query and returns its plan without running it, so the plan contains estimates. PROFILE executes the query and augments the plan with runtime measurements such as actual rows and database hits. This distinction is operationally important: if the query contains writes, PROFILE performs those writes. Start with EXPLAIN, especially when investigating an unfamiliar statement.

Signal EXPLAIN PROFILE Interpretation
Estimated Rows Yes Yes Planner estimate used for plan choice; not runtime truth
Rows No Yes Rows actually produced by an operator
DB Hits No Yes Abstract storage-engine work, not the same thing as rows
Memory (Bytes) No When measurable Per-operator peak heap; footer total is query-wide peak, not a sum
Time / Pipeline No Runtime-dependent Pipelined/parallel figures can represent fused pipelines, not individual operators
Page Cache Hits/Misses No Enterprise-only column Do not expect this column in the Community lab

2. Read the tree from the leaves upward

The bottom operators find starting entities; their parents filter, expand, join, aggregate, sort and finally produce results. A plan is a binary tree: a leaf has no children, unary operators transform one stream, and join/apply operators may have two inputs. The row flow is upward even though the table is printed top-down.

Cypher · create and verify the deterministic Chapter 10 fixture
CYPHER 25CREATE CONSTRAINT customer_id IF NOT EXISTS FOR (c:Customer) REQUIRE c.customerId IS UNIQUE;CREATE CONSTRAINT order_id IF NOT EXISTS FOR (o:Order) REQUIRE o.orderId IS UNIQUE;CREATE CONSTRAINT product_id IF NOT EXISTS FOR (p:Product) REQUIRE p.productId IS UNIQUE;UNWIND range(1,100) AS iMERGE (c:Customer {customerId:'CH10-C-'+right('0000'+toString(i),4)})SET c.name='Plan Customer '+toString(i),    c.segment=CASE WHEN i=1 THEN 'HOT' WHEN i % 10=0 THEN 'VIP' ELSE 'STANDARD' END,    c.labTag='ch10';UNWIND range(1,200) AS iMERGE (p:Product {productId:'CH10-P-'+right('0000'+toString(i),4)})SET p.name='Plan Product '+toString(i),    p.categoryCode=CASE WHEN i % 10 < 6 THEN 'CAM' WHEN i % 10 < 8 THEN 'PWR' WHEN i % 10 = 8 THEN 'ACC' ELSE 'OUT' END,    p.price=toFloat(20 + (i % 180)),    p.labTag='ch10';UNWIND range(1,2000) AS iWITH i, CASE WHEN i <= 800 THEN 1 ELSE 2 + ((i-801) % 99) END AS customerNoMATCH (c:Customer {customerId:'CH10-C-'+right('0000'+toString(customerNo),4)})MERGE (o:Order {orderId:'CH10-O-'+right('0000'+toString(i),4)})SET o.status=CASE WHEN i % 7=0 THEN 'REFUNDED' WHEN i % 3=0 THEN 'SHIPPED' ELSE 'PAID' END,    o.createdAt=datetime({epochSeconds:1735689600 + i*60}),    o.labTag='ch10'MERGE (c)-[:PLACED]->(o)WITH i,oMATCH (p1:Product {productId:'CH10-P-'+right('0000'+toString(1 + ((i-1) % 200)),4)})MATCH (p2:Product {productId:'CH10-P-'+right('0000'+toString(1 + ((i*7-1) % 200)),4)})MERGE (o)-[r1:CONTAINS {lineId:'CH10-L-'+right('0000'+toString(i),4)+'-A'}]->(p1)SET r1.quantity=1 + (i % 3),r1.labTag='ch10'MERGE (o)-[r2:CONTAINS {lineId:'CH10-L-'+right('0000'+toString(i),4)+'-B'}]->(p2)SET r2.quantity=1,r2.labTag='ch10';CREATE RANGE INDEX ch10_product_category_price IF NOT EXISTSFOR (p:Product) ON (p.categoryCode,p.price);CREATE RANGE INDEX ch10_order_status IF NOT EXISTSFOR (o:Order) ON (o.status);CALL db.awaitIndexes(300);
Cypher · verify graph-size and skew invariants
CYPHER 25MATCH (c:Customer {labTag:'ch10'}) WITH count(c) AS customersMATCH (p:Product {labTag:'ch10'}) WITH customers,count(p) AS productsMATCH (o:Order {labTag:'ch10'}) WITH customers,products,count(o) AS ordersMATCH ()-[r:CONTAINS {labTag:'ch10'}]->()RETURN customers,products,orders,count(r) AS containsRelationships;MATCH (c:Customer {labTag:'ch10'})-[:PLACED]->(o:Order {labTag:'ch10'})RETURN c.customerId,count(o) AS ordersORDER BY orders DESC,c.customerIdLIMIT 5;

3. Baseline one query before tuning it

Use the same query text and parameters for the paired plan capture. The fixture intentionally gives CH10-C-0001 800 orders while the remaining 99 customers share the other 1200 orders. This makes graph-shape skew visible without needing a production-sized store.

cypher-shell · define representative parameters
:param customerId => "CH10-C-0001";:param category => "CAM";:param minPrice => 80.0;
Cypher · plan only
CYPHER 25EXPLAINMATCH (c:Customer {customerId:$customerId})-[:PLACED]->(o:Order)-[r:CONTAINS]->(p:Product)WHERE p.categoryCode=$category AND p.price >= $minPriceRETURN count(DISTINCT o) AS orders,       count(*) AS lineRows,       sum(r.quantity) AS units;
Cypher · execute and measure
CYPHER 25PROFILEMATCH (c:Customer {customerId:$customerId})-[:PLACED]->(o:Order)-[r:CONTAINS]->(p:Product)WHERE p.categoryCode=$category AND p.price >= $minPriceRETURN count(DISTINCT o) AS orders,       count(*) AS lineRows,       sum(r.quantity) AS units;

4. What the numbers prove—and what they do not

Observation What it can support What it cannot prove alone
Estimated Rows far from Rows Planner cardinality model is inaccurate for this execution That the index is wrong or the query is globally slow
High DB Hits Substantial storage-engine work occurred Disk I/O specifically; many hits can be cache-resident
Large operator memory A blocking/materializing operator has heap cost Total process memory or page-cache pressure
Few final rows API payload is small Intermediate work was small
Fast one-off PROFILE This run finished quickly Stable p95/p99 latency under representative concurrency/cache state

5. Deliberately wrong: tune from elapsed time alone

Run the same query twice and you may observe a faster second execution because planning, page cache, filesystem cache, JVM state and other conditions changed. The repair is to record the plan, input parameters, graph size/degree distribution, cache/warmup protocol, result cardinality and a latency distribution. Change one dimension at a time.

Check your understanding

  1. Does EXPLAIN execute the query?
  2. Can PROFILE mutate data?
  3. Are DB Hits identical to returned rows?
  4. Can Community learners expect page-cache hit/miss columns in PROFILE?
  5. Why read a plan bottom-up?
Review the answers

1. No. It plans the query and reports estimates.

2. Yes. PROFILE executes the statement, including writes.

3. No. One row can require many storage-engine accesses, and many rows can sometimes reuse accessed data.

4. No. Current documentation marks that plan column Enterprise-only.

5. Leaf operators establish starting data and rows flow upward through transformations to ProduceResults.

Production judgment and next step

Do not optimize for the prettiest operator name. Optimize an observable workload: correctness first, then cardinality, work, memory, tail latency and maintainability. Next, we explain why the same syntax can produce radically different work when graph degree and selectivity differ.

Summary and next step

EXPLAIN vs PROFILE, Logical/Physical Operators, Rows, Database Hits, Memory, and Pipeline Interpretation 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 Cardinality Estimation, Selectivity, Starting Points, Expand Operators, and Why Graph Shape Matters. 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

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.