neo4j-query-tuning-skill

Diagnoses and fixes slow Neo4j Cypher queries by reading execution plans, identifying bad operators (AllNodesScan, CartesianProduct, Eager, NodeByLabelScan), and prescribing fixes (indexes, hints, query rewrites, runtime selection). Use when a query is slow, when EXPLAIN or PROFILE output needs inte

By neo4j-contrib · 605 installs

npx skills add neo4j-contrib/neo4j-skills --skill neo4j-query-tuning-skill

Source repository · Upstream listing

When to Use Query takes unexpectedly long; need root cause analysis EXPLAIN/PROFILE output in hand — needs interpretation Identifying which index is missing or unused Deciding between slotted / pipelined / parallel runtimes Monitoring live queries: SHOW QUERIES, SHOW TRANSACTIONS Cardinality estimates wrong (plan replanning needed) When NOT to Use Writing Cypher from scratch → neo4j cypher skill GDS algorithm performance → neo4j gds skill Schema design / data modelling → neo4j modeling skill EXPLAIN vs PROFILE EXPLAIN PROFILE Executes query? No Yes Returns data? No Yes Shows rows (actual) No Yes Shows dbHits (actual) No Yes Shows estimatedRows Yes Yes Cost Zero Full query cost Run PROFILE twice — first run warms page cache; second gives representative metrics. Query API alternative (no driver): Key Plan Metrics Metric Good Investigate if dbHits Low; drops after index added High relative to rows rows Shrinks early in plan Large until final operator estimatedRows Close to rows 10× divergence from actual pageCacheHitRatio 0.99 <0.90 (disk I/O bottleneck) pageCacheHits High — pageCacheMisses Near 0 Rising (page cache too small) Read plans bottom up — leaf operators at bottom initiate data retrieval. Operator Reference Operator Good/Bad Meaning Fix NodeIndexSeek ✓ Exact match via RANGE/LOOKUP index — NodeUniqueIndexSeek ✓ Unique constraint index hit — NodeIndexContainsScan ✓ TEXT index CONTAINS / STARTS WITH — NodeIndexScan ~ Full index scan (no predicate) Add WHERE predicate or composite index NodeByLabelScan ✗ Scans all nodes of label Add RANGE index on lookup property AllNodesScan ✗✗ Scans entire node store Add label + index to MATCH Expand(All) ~ Traverse relationships from node Normal; limit with LIMIT or WHERE Expand(Into) ~ Find rels between two matched nodes Normal for known endpoint joins Filter ~ Predicate applied after scan Move predicate into WHERE with index CartesianProduct ✗ No join predicate between two MATCH Add WHERE join or use WITH between MATCHes NodeHashJoin ~ Hash join on node IDs Normal; planner chose hash join ValueHashJoin ~ Hash join on values Normal; watch memory for large inputs EagerAggregation ~ Full aggregation (ORDER BY, count( )) Normal for aggregates Aggregation ✓ Streaming aggregation — Eager ✗ Read/write conflict; materialises all rows See Eager fix strategies below Sort ~ Full sort — O(n log n) Add LIMIT before Sort; push LIMIT earlier Top ✓ Sort+Limit combined — O(n log k) Preferred over Sort+Limit Limit ✓ Truncates rows early Push as early as possible Skip ~ Offset pagination Use keyset pagination on large graphs ProduceResults — Final output operator Root of tree UndirectedRelationshipByIdSeekPipe ~ Lookup by relationship ID Avoid id(r) — use elementId(r) Full operator reference → [references/plan operators.md](references/plan operators.md) Diagnostic Workflow (Agent Runbook) Step 1 — Baseline Plan Scan output for AllNodesScan , NodeByLabelScan , CartesianProduct , Eager . Step 2 — Check Indexes Find whether the label/property from the bad operator has an index. Step 3 — Create Missing Index Wait for state = 'ONLINE' before measuring. Step 4 — Profile After Fix Compare dbHits and elapsed ms before/after. Target: NodeIndexSeek replaces scan operators. Step 5 — Stale Statistics (if estimatedRows wildly off) Config: dbms.cypher.statistics divergence threshold (default 0.75 — plan expires when stat changes 75%). Fixing Common Plan Problems Missing Index → NodeByLabelScan / AllNodesScan Wrong Anchor — Planner Picks Wrong Starting Node Reorder MATCH or use hints: CartesianProduct — Two Unconnected MATCHes Eager — Read/Write Conflict Three strategies (pick simplest): 1. Add specific labels to MATCH nodes so planner distinguishes read/write sets 2. Collect then write : WITH collect(n) AS nodes UNWIND nodes AS n SET n.x = 1 3. CALL IN TRANSACTIONS : isolates each batch in its own transaction Expensive CONTAINS / ENDS WITH Over Traversal — Push LIMIT Early Cypher Runtime Selection Runtime Select Best For Avoid When pipelined CYPHER runtime=pipelined Default OLTP; streaming, low memory Unsupported operators fall back to slotted slotted CYPHER runtime=slotted Guaranteed stable behavior; debug Performance critical OLTP parallel CYPHER 25 runtime=parallel Large analytical scans; aggregations OLTP, writes, short queries, Aura Free Pipelined is default for most queries. Parallel requires dbms.cypher.parallel.worker limit configured; available on Enterprise and Aura Pro 2025+. Query Monitoring Commands Full monitoring reference → [references/stats and monitoring.md](references/stats and monitoring.md) Checklist [ ] Run EXPLAIN first — identifies plan problems without execution cost [ ] Check for AllNodesScan / NodeByLabelScan — missing index [ ] Check for CartesianProduct — missing join predicate [ ] Check for Eager — read/write conflict [ ] SHOW INDEXES — confirm relevant index exists and state = 'ONLINE' [ ] Create missing index; wait for ONLINE [ ] Run PROFILE twice — first warms cache, second is representative [ ] Compare dbHits before/after fix [ ] If estimatedRows wildly off → CALL db.prepareForReplanning() [ ] Push LIMIT / WITH n LIMIT k before high fanout operations [ ] For CONTAINS/ENDS WITH — TEXT index, not RANGE [ ] For large analytical queries — consider runtime=parallel [ ] Kill long running queries with TERMINATE TRANSACTION