$npx -y skills add neo4j-contrib/neo4j-skills --skill neo4j-query-tuning-skillDiagnoses and fixes slow Neo4j Cypher queries by reading execution plans, identifying
| 1 | ## When to Use |
| 2 | - Query takes unexpectedly long; need root-cause analysis |
| 3 | - EXPLAIN/PROFILE output in hand — needs interpretation |
| 4 | - Identifying which index is missing or unused |
| 5 | - Deciding between slotted / pipelined / parallel runtimes |
| 6 | - Monitoring live queries: SHOW QUERIES, SHOW TRANSACTIONS |
| 7 | - Cardinality estimates wrong (plan replanning needed) |
| 8 | |
| 9 | ## When NOT to Use |
| 10 | - **Writing Cypher from scratch** → `neo4j-cypher-skill` |
| 11 | - **GDS algorithm performance** → `neo4j-gds-skill` |
| 12 | - **Schema design / data modelling** → `neo4j-modeling-skill` |
| 13 | |
| 14 | --- |
| 15 | |
| 16 | ## EXPLAIN vs PROFILE |
| 17 | |
| 18 | | | EXPLAIN | PROFILE | |
| 19 | |---|---|---| |
| 20 | | Executes query? | No | Yes | |
| 21 | | Returns data? | No | Yes | |
| 22 | | Shows `rows` (actual) | No | Yes | |
| 23 | | Shows `dbHits` (actual) | No | Yes | |
| 24 | | Shows `estimatedRows` | Yes | Yes | |
| 25 | | Cost | Zero | Full query cost | |
| 26 | |
| 27 | Run `PROFILE` **twice** — first run warms page cache; second gives representative metrics. |
| 28 | |
| 29 | ```cypher |
| 30 | EXPLAIN MATCH (p:Person {email: $email}) RETURN p.name |
| 31 | PROFILE MATCH (p:Person {email: $email}) RETURN p.name |
| 32 | ``` |
| 33 | |
| 34 | Query API alternative (no driver): |
| 35 | ```bash |
| 36 | curl -X POST https://<host>/db/<db>/query/v2 \ |
| 37 | -u <user>:<pass> -H "Content-Type: application/json" \ |
| 38 | -d '{"statement": "EXPLAIN MATCH (p:Person {email: $email}) RETURN p.name", "parameters": {"email": "a@b.com"}}' |
| 39 | ``` |
| 40 | |
| 41 | --- |
| 42 | |
| 43 | ## Key Plan Metrics |
| 44 | |
| 45 | | Metric | Good | Investigate if | |
| 46 | |---|---|---| |
| 47 | | `dbHits` | Low; drops after index added | High relative to `rows` | |
| 48 | | `rows` | Shrinks early in plan | Large until final operator | |
| 49 | | `estimatedRows` | Close to `rows` | >10× divergence from actual | |
| 50 | | `pageCacheHitRatio` | >0.99 | <0.90 (disk I/O bottleneck) | |
| 51 | | `pageCacheHits` | High | — | |
| 52 | | `pageCacheMisses` | Near 0 | Rising (page cache too small) | |
| 53 | |
| 54 | Read plans **bottom-up** — leaf operators at bottom initiate data retrieval. |
| 55 | |
| 56 | --- |
| 57 | |
| 58 | ## Operator Reference |
| 59 | |
| 60 | | Operator | Good/Bad | Meaning | Fix | |
| 61 | |---|---|---|---| |
| 62 | | `NodeIndexSeek` | ✓ | Exact match via RANGE/LOOKUP index | — | |
| 63 | | `NodeUniqueIndexSeek` | ✓ | Unique constraint index hit | — | |
| 64 | | `NodeIndexContainsScan` | ✓ | TEXT index CONTAINS / STARTS WITH | — | |
| 65 | | `NodeIndexScan` | ~ | Full index scan (no predicate) | Add WHERE predicate or composite index | |
| 66 | | `NodeByLabelScan` | ✗ | Scans all nodes of label | Add RANGE index on lookup property | |
| 67 | | `AllNodesScan` | ✗✗ | Scans entire node store | Add label + index to MATCH | |
| 68 | | `Expand(All)` | ~ | Traverse relationships from node | Normal; limit with LIMIT or WHERE | |
| 69 | | `Expand(Into)` | ~ | Find rels between two matched nodes | Normal for known-endpoint joins | |
| 70 | | `Filter` | ~ | Predicate applied after scan | Move predicate into WHERE with index | |
| 71 | | `CartesianProduct` | ✗ | No join predicate between two MATCH | Add WHERE join or use WITH between MATCHes | |
| 72 | | `NodeHashJoin` | ~ | Hash join on node IDs | Normal; planner chose hash join | |
| 73 | | `ValueHashJoin` | ~ | Hash join on values | Normal; watch memory for large inputs | |
| 74 | | `EagerAggregation` | ~ | Full aggregation (ORDER BY, count(*)) | Normal for aggregates | |
| 75 | | `Aggregation` | ✓ | Streaming aggregation | — | |
| 76 | | `Eager` | ✗ | Read/write conflict; materialises all rows | See Eager fix strategies below | |
| 77 | | `Sort` | ~ | Full sort — O(n log n) | Add `LIMIT` before Sort; push LIMIT earlier | |
| 78 | | `Top` | ✓ | Sort+Limit combined — O(n log k) | Preferred over Sort+Limit | |
| 79 | | `Limit` | ✓ | Truncates rows early | Push as early as possible | |
| 80 | | `Skip` | ~ | Offset pagination | Use keyset pagination on large graphs | |
| 81 | | `ProduceResults` | — | Final output operator | Root of tree | |
| 82 | | `UndirectedRelationshipByIdSeekPipe` | ~ | Lookup by relationship ID | Avoid `id(r)` — use `elementId(r)` | |
| 83 | |
| 84 | Full operator reference → [references/plan-operators.md](references/plan-operators.md) |
| 85 | |
| 86 | --- |
| 87 | |
| 88 | ## Diagnostic Workflow (Agent Runbook) |
| 89 | |
| 90 | ### Step 1 — Baseline Plan |
| 91 | ```cypher |
| 92 | EXPLAIN <query> |
| 93 | ``` |
| 94 | Scan output for `AllNodesScan`, `NodeByLabelScan`, `CartesianProduct`, `Eager`. |
| 95 | |
| 96 | ### Step 2 — Check Indexes |
| 97 | ```cypher |
| 98 | SHOW INDEXES YIELD name, type, labelsOrTypes, properties, state |
| 99 | WHERE state = 'ONLINE' |
| 100 | ``` |
| 101 | Find whether the label/propert |