Chapter 26 · Performance Engineering: Data Model, Traversal Shape, Page Cache, Memory, and Workload Isolation

Query-Level Optimization: Anchors, Selectivity, Predicate Placement, Subqueries, Aggregation, and Result Shaping

Use Cypher 25 EXPLAIN/PROFILE evidence to improve selective anchors, predicates, subqueries, aggregation, and result shaping without relying on textual clause order or universal plan folklore.

Advanced280–400 minutesCypher PROFILE · selectivity · result shapingNeo4j 2026.07.1 · Community mandatoryCypher 25 · Python driver 6.3.x optionalDisposable perf container · 2 GiB/2 CPU baselineJava 21/25 · no APOC/GDS requiredLast reviewed: September 2026

Learning outcomes

01

Choose selective query anchors from constraints/indexes and data distributions rather than from the visual left-to-right order of MATCH patterns.

02

Use PROFILE Rows, DB Hits, memory and operator shape to find row explosion while recognizing edition/runtime-specific columns.

03

Place predicates and subqueries to reduce intermediate cardinality without changing OPTIONAL/aggregation semantics.

04

Shape results to return only business-needed values and separate database execution cost from serialization/network/client materialization.

05

Create a repeatable query regression test that checks correctness first and plan/latency evidence second.

1. AtlasMart problem: a small response can hide a huge intermediate pipeline

An endpoint returns only ten products, yet the query first expands every recent order, every contained product, every category, groups thousands of rows, sorts them, and finally applies LIMIT 10. Cypher is a row/pattern pipeline: the final row count does not reveal the amount of intermediate work.

Selectivity is how strongly a predicate narrows candidate entities. An anchor is the part of the pattern the planner can use to begin from a constrained/selective set. Cypher is declarative, so writing a variable first in text does not force execution order. Use indexes/constraints and inspect the plan rather than “reordering MATCH” by superstition.

Dimension Chapter 26 reproducible assumption
Neo4j 2026.07.1 Community. The continuity database remains neo4j; performance experiments use a separate disposable container named atlasmart-neo4j-perf so tuning/failure tests do not disturb earlier labs.
Cypher Cypher 25 examples. Planner/operator names and numeric PROFILE values are evidence to capture locally, not constants to memorize.
Java Neo4j 2026 line with a supported Java 21/25 runtime as supplied/required by the chosen distribution.
Driver Neo4j Python driver 6.3.x for the optional load harness; the driver object is shared, sessions/transactions are not shared between worker threads.
Auth/TLS User neo4j, password atlasmart-course-2026. Loopback Bolt without TLS only for the isolated disposable lab; remote/production traffic should use validated TLS.
Ports Performance container maps HTTP 17474→7474 and Bolt 17687→7687, avoiding the continuity container on 7474/7687.
Initial resource envelope Exercise baseline: Docker limit 2 GiB, 2 CPUs, explicit heap 512 MiB, explicit page cache 512 MiB. These are lab controls, not production recommendations.
Dataset 200 CH26 customers, 300 products, 2,000 orders, 6,000 CONTAINS relationships, 1,200 VIEWED_CH26 relationships, six categories, plus an isolated hot-counter node. Recount locally after setup.
Observability Community labs rely on PROFILE, SHOW commands, Docker/OS counters, driver timing, container stats and logs. Neo4j metrics exporters and query.log are Enterprise surfaces and are optional, clearly labeled.
Evidence rule This generated material does not execute your Docker host. Latencies, DB Hits, page-cache behavior, saturation points, GC, throughput, errors and recovery time must be measured locally; illustrative tables are labeled as templates.
Verify index state before comparing queries
RETURN 1 AS cypherReachable;
CALL dbms.components() YIELD name, versions, edition
RETURN name, versions, edition;
SHOW SETTINGS YIELD name, value
WHERE name IN [
  'server.memory.heap.initial_size',
  'server.memory.heap.max_size',
  'server.memory.pagecache.size',
  'dbms.memory.transaction.total.max',
  'db.memory.transaction.total.max',
  'db.memory.transaction.max'
]
RETURN name, value ORDER BY name;
SHOW INDEXES YIELD name, state, type, entityType, labelsOrTypes, properties
WHERE name STARTS WITH 'ch26_'
RETURN name, state, type, entityType, labelsOrTypes, properties ORDER BY name;

2. Bad anchor vs usable anchor: change semantics as little as possible

The first query applies a function to a property and asks for a product by case-folded name. That expression may prevent a normal range-index seek on the raw property and can widen work. The repaired query uses the stable constrained product key.

Compare non-sargable filtering with an indexed business key
// Diagnostic only: inspect locally with PROFILE.
PROFILE
MATCH (p:CH26Product)<-[:CONTAINS]-(o:CH26Order)
WHERE toLower(p.name)=toLower($name)
RETURN o.orderId LIMIT 20;

PROFILE
MATCH (p:CH26Product {productId:$productId})<-[:CONTAINS]-(o:CH26Order)
RETURN o.orderId LIMIT 20;

Do not infer a speedup from the text alone. Confirm the second query starts from an index-backed selective access path, then compare actual rows/DB hits and repeated latency. If the application truly requires case-insensitive name search, model/index the requirement deliberately (for example normalized property or text/full-text index where semantics fit) instead of hiding it inside an arbitrary function.

3. Predicate placement: reduce rows without changing meaning

Predicates can be pushed/optimized by the planner, but scoping still matters. The dangerous cases are semantic: predicates attached to OPTIONAL MATCH, variables introduced by previous clauses, or filtering after an expansion/aggregation that could have been bounded earlier.

Bound an order population before the expensive expansion
// Baseline shape: inspect intermediate Rows in PROFILE.
PROFILE
MATCH (c:CH26Customer)-[:PLACED]->(o:CH26Order)-[:CONTAINS]->(p:CH26Product)
WHERE o.createdDay >= $cutoff AND o.status='COMPLETE'
RETURN p.productId, count(*) AS times
ORDER BY times DESC LIMIT 20;

// When the business question really is "top products among the 200 most recent qualifying orders",
// make that bounded scope explicit with a subquery.
PROFILE
CALL {
  MATCH (o:CH26Order)
  WHERE o.createdDay >= $cutoff AND o.status='COMPLETE'
  WITH o ORDER BY o.createdDay DESC, o.orderId DESC LIMIT 200
  RETURN o
}
MATCH (o)-[:CONTAINS]->(p:CH26Product)
RETURN p.productId, count(*) AS times
ORDER BY times DESC LIMIT 20;
Semantic boundary

The subquery is not a “free optimization”: it changes the question unless the product requirement explicitly defines the recent-order cap. Performance rewrites must preserve intended semantics, not merely produce faster output.

4. Aggregation, subqueries, and accidental cardinality multiplication

Every additional independent pattern can multiply rows. If AtlasMart independently matches a customer’s viewed products and placed orders in the same row scope, it can form combinations before aggregation. Separate independent aggregates into subqueries or aggregate one side before expanding the other.

Detect and repair a multiplication-prone query
// Multiplication-prone: views × orders for one customer.
PROFILE
MATCH (c:CH26Customer {customerId:$cid})-[:VIEWED_CH26]->(p:CH26Product),
      (c)-[:PLACED]->(o:CH26Order)
RETURN count(DISTINCT p) AS productsViewed,
       count(DISTINCT o) AS orders;

// Same business outputs, independent bounded pipelines.
PROFILE
MATCH (c:CH26Customer {customerId:$cid})
CALL (c) {
  MATCH (c)-[:VIEWED_CH26]->(p:CH26Product)
  RETURN count(DISTINCT p) AS productsViewed
}
CALL (c) {
  MATCH (c)-[:PLACED]->(o:CH26Order)
  RETURN count(DISTINCT o) AS orders
}
RETURN productsViewed, orders;

5. Result shaping belongs to query performance

Returning entire nodes/paths asks Neo4j to read more properties, serialize more data, send more bytes, and asks the driver/application to materialize more objects. When the API needs three scalars, project three scalars. A small driver fetch_size can bound batches for lazy results, but it cannot make an intrinsically blocking server operator such as a large sort stream before its required work is complete.

Return an API-shaped projection rather than graph objects
MATCH (c:CH26Customer {customerId:$cid})-[:PLACED]->(o:CH26Order)-[:CONTAINS]->(p:CH26Product)
RETURN o.orderId AS orderId,
       o.status AS status,
       collect({productId:p.productId, quantity:1})[0..5] AS products
ORDER BY orderId DESC
LIMIT 20;

6. Deliberately wrong approach: optimize by rearranging MATCH text

A practitioner moves (p:Product) to the left side of the pattern and reports the query is now “anchored.” Cypher’s cost planner can choose execution order independently of textual pattern order. The rewrite may have changed nothing.

Repair: make a selective predicate/index available, run EXPLAIN to inspect the intended access path, run PROFILE in a controlled environment to collect actual Rows/DB Hits/memory, and measure end-to-end latency with the same parameters/result size. Keep plan operator names/runtime/version in the evidence because they can evolve.

Production judgment

Query-level tuning should reduce work while preserving correctness under real parameter distributions. A plan optimized for a rare selective key may behave differently for a broad category. Parameterize queries for plan cache reuse and injection safety; maintain statistics/indexes; bound variable-length traversals; treat large sorts/aggregations as memory consumers; keep transaction timeouts/retries/idempotency in the application contract; and separate server execution from driver pool/network/client costs. Lesson 4 asks whether the remaining bottleneck is genuinely infrastructure rather than query shape.

Check your understanding

  1. Does the first variable written in MATCH determine the execution start?
  2. Why can LIMIT 10 still be expensive?
  3. What does DB Hits measure?
  4. Why can a subquery “optimization” be wrong?
  5. Why shape results narrowly?
Review the answers

1. No. Cypher is declarative; the planner chooses access paths/order from predicates, statistics, indexes and runtime rules.

2. The plan may need to expand, aggregate or sort a much larger intermediate row set before it can know which ten rows to return.

3. Low-level storage/index/entity access work reported by PROFILE; it is not the same as output row count or wall-clock latency.

4. If it limits/filters a population differently, it changes the business question even if it runs faster.

5. It can reduce property reads, serialization, network transfer and client memory independently of graph matching cost.

Summary and next step

Query-Level Optimization: Anchors, Selectivity, Predicate Placement, Subqueries, Aggregation, and Result Shaping 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 Infrastructure Tuning: Heap, Page Cache, Transaction Memory, CPU, Storage, Containers, and OS Limits. 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.