Skip to main content
skill CX Codex

neo4j-query-tuning-skill — Claude Code skill: fix slow Cypher queries

Reads a Neo4j execution plan, spots the operators that slow a Cypher query down, and tells you what to change. Useful when a query drags and PROFILE output is hard to read.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add neo4j-contrib/neo4j-skills --skill neo4j-query-tuning-skill -a claude-code
Open on GitHub

What neo4j-query-tuning-skill does

The skill parses EXPLAIN and PROFILE output and looks for the usual suspects: AllNodesScan, NodeByLabelScan, CartesianProduct, Eager. Then it prescribes a fix — add an index, force a USING INDEX or USING SCAN hint, rewrite the query, switch runtime. It also walks through the metrics that matter: dbHits, rows, estimatedRows, pageCacheHitRatio. When the estimated cardinality diverges from the actual by more than 10×, that is your cue to replan stats with db.prepareForReplanning() or db.resampleIndex().

The runbook is ordered: capture a baseline plan, check indexes with SHOW INDEXES, create the missing one, then re-run PROFILE to compare. It advises profiling twice — the first run warms the page cache, the second gives numbers you can trust. For live work it covers SHOW QUERIES, SHOW TRANSACTIONS and TERMINATE TRANSACTION. Only Bash and WebFetch are allowed as tools.

This is not a general Cypher helper. It will not write queries from scratch — that is neo4j-cypher-skill. GDS algorithm tuning and index/constraint creation syntax are out of scope too. Come here when an existing query is slow, not when you are designing a schema.

What it helps with

  • find out why a Cypher query runs longer than expected
  • interpret EXPLAIN or PROFILE output and name the operator that hurts
  • figure out which index is missing for a property lookup
  • choose between slotted, pipelined and parallel runtimes
  • inspect running queries and transactions, then kill one if needed

Example prompts for the agent

  • Read the plan for this slow query: PROFILE MATCH (p:Person {email: $email}) RETURN p.name
  • Why is there an AllNodesScan here, and which index should I add?
  • Check SHOW QUERIES and tell me which of these to terminate

How to install neo4j-query-tuning-skill

1Skills CLI (Vercel)

npx skills add neo4j-contrib/neo4j-skills --skill neo4j-query-tuning-skill -a claude-code

2Manually: put the files in place

~/.claude/skills/neo4j-query-tuning-skill/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/neo4j-query-tuning-skill/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about neo4j-query-tuning-skill

How do I install neo4j-query-tuning-skill in Claude?

Run npx skills add neo4j-contrib/neo4j-skills --skill neo4j-query-tuning-skill -a claude-code. The skill lands at ~/.claude/skills/neo4j-query-tuning-skill/SKILL.md.

What does neo4j-query-tuning-skill do?

It diagnoses slow Neo4j Cypher queries: reads the execution plan, identifies bad operators, and prescribes fixes such as indexes, hints, query rewrites or a different runtime.

How is it different from neo4j-cypher-skill?

It does not write Cypher from scratch and does not cover index or constraint creation syntax — that is neo4j-cypher-skill. This one tunes queries you already have.

Source text

SKILL.md · MIT
---
name: neo4j-query-tuning-skill
description: 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 interpretation, when dbHits or pageCacheHitRatio are poor, when
  cardinality estimation diverges from actuals, or when deciding between slotted/pipelined/parallel
  runtimes. Covers USING INDEX / USING SCAN / USING JOIN hints, db.stats.retrieve, SHOW QUERIES,
  SHOW TRANSACTIONS, TERMINATE TRANSACTION.
  Does NOT write new Cypher from scratch — use neo4j-cypher-skill.
  Does NOT cover GDS algorithm tuning — use neo4j-gds-skill.
  Does NOT cover index/constraint creation syntax details — use neo4j-cypher-skill references/indexes.md.
allowed-tools: Bash WebFetch
version: 1.0.8
---

## 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.

```cypher
EXPLAIN MATCH (p:Person {email: $email}) RETURN p.name
PROFILE MATCH (p:Person {email: $email}) RETURN p.name
```

Query API alternative (no driver):
```bash
curl -X POST https://<host>/db/<db>/query/v2 \
  -u <user>:<pass> -H "Content-Type: application/json" \
  -d '{"statement": "EXPLAIN MATCH (p:Person {email: $email}) RETURN p.name", "parameters": {"email": "[email protected]"}}'
```

---

## 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
```cypher
EXPLAIN <query>
```
Scan output for `AllNodesScan`, `NodeByLabelScan`, `CartesianProduct`, `Eager`.

### Step 2 — Check Indexes
```cypher
SHOW INDEXES YIELD name, type, labelsOrTypes, properties, state
WHERE state = 'ONLINE'
```
Find whether the label/property from the bad operator has an index.

### Step 3 — Create Missing Index
```cypher
// RANGE index for equality/range predicates:
CREATE INDEX person_email IF NOT EXISTS FOR (n:Person) ON (n.email)
// TEXT index for CONTAINS/ENDS WITH:
CREATE TEXT INDEX person_bio IF NOT EXISTS FOR (n:Person) ON (n.bio)
// Composite for multi-property lookup:
CREATE INDEX order_status_date IF NOT EXISTS FOR (n:Order) ON (n.status, n.createdAt)
```
Wait for `state = 'ONLINE'` before measuring.

### Step 4 — Profile After Fix
```cypher
PROFILE <query>
```
Compare `dbHits` and elapsed ms before/after. Target: `NodeIndexSeek` replaces scan operators.

### Step 5 — Stale Statistics (if estimatedRows wildly off)
```cypher
CALL db.prepareForReplanning()
// or resample a specific index:
CALL db.resampleIndex("person_email")
// or resample all outdated:
CALL db.resampleOutdatedIndexes()
```
Config: `dbms.cypher.statistics_divergence_threshold` (default `0.75` — plan expires when stat changes >75%).

---

## Fixing Common Plan Problems

### Missing Index → NodeByLabelScan / AllNodesScan
```cypher
// Force index hint when planner ignores it:
MATCH (p:Person {email: $email})
USING INDEX p:Person(email)
RETURN p.name
// Force label scan (sometimes faster for high selectivity):
MATCH (p:Person {email: $email})
USING SCAN p:Person
RETURN p.name
```

### Wrong Anchor — Planner Picks Wrong Starting Node
Reorder MATCH or use hints:
```cypher
// Force join at specific node:
MATCH (a:Author)-[:WROTE]->(b:Book)-[:IN_CATEGORY]->(c:Category {name: $cat})
USING JOIN ON b
RETURN a.name, b.title
```

### CartesianProduct — Two Unconnected MATCHes
```cypher
// Bad (Cartesian product):
MATCH (a:Author {id: $aid})
MATCH (b:Book  {id: $bid})
RETURN a.name, b.title

// Good (explicit join or WITH):
MATCH (a:Author {id: $aid})-[:WROTE]->(b:Book {id: $bid})
RETURN a.name, b.title
// Or: WITH between them to reset planning context
```

### 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
```cypher
CYPHER 25
MATCH (p:Person) WHERE p.score > 100
CALL (p) { SET p.tier = 'gold' } IN TRANSACTIONS OF 1000 ROWS
```

### Expensive CONTAINS / ENDS WITH
```cypher
// Needs TEXT index (RANGE does NOT support these):
CREATE TEXT INDEX person_bio IF NOT EXISTS FOR (n:Person) ON (n.bio)
MATCH (p:Person) WHERE p.bio CONTAINS $keyword RETURN p.name
```

### Over-Traversal — Push LIMIT Early
```cypher
// Bad: LIMIT after expensive join
MATCH (a:Author)-[:WROTE]->(b:Book)-[:REVIEWED_BY]->(r:Review)
RETURN a.name, b.title, r.text LIMIT 10

// Good: anchor limit before fan-out
MATCH (a:Author)-[:WROTE]->(b:Book)
WITH a, b LIMIT 10
MATCH (b)-[:REVIEWED_BY]->(r:Review)
RETURN a.name, b.title, r.text
```

---

## 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+.

```cypher
// Force parallel for large aggregation:
CYPHER 25 runtime=parallel
MATCH (n:Transaction) WHERE n.amount > 1000
RETURN n.currency, count(*), sum(n.amount)
```

---

## Query Monitoring Commands

```cypher
// Live queries + resource usage:
SHOW QUERIES YIELD query, queryId, elapsedTimeMillis, allocatedBytes, status, username

// Running transactions:
SHOW TRANSACTIONS YIELD transactionId, currentQuery, currentQueryProgress, elapsedTime, status, username, cpuTime, activeLockCount  // currentQueryProgress added [2026.03]

// Kill a specific transaction:
TERMINATE TRANSACTION $transactionId

// Kill a query:
TERMINATE QUERY $queryId

// Graph count stats (node/rel counts by label/type — feed into planner):
CALL db.stats.retrieve('GRAPH COUNTS') YIELD section, data RETURN section, data

// Token stats (label/property/rel-type IDs):
CALL db.stats.retrieve('TOKENS') YIELD section, data RETURN section, data
```

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`

© neo4j-contrib · MIT · neo4j-contrib/neo4j-skills

Similar skills

All skills for Data and databases →

From the same set18

neo4j-cypher-skill Writes, optimizes and validates Cypher 25 queries for Neo4j 2025.x and 2026.x. Useful when a slow MATCH needs fixing or a batch write has to be assembled. CX Codex +1 1,500installs neo4j-graphrag-skill A skill that walks you through building GraphRAG retrieval pipelines on Neo4j with the neo4j-graphrag package: retriever choice, Cypher fragments, LLM wiring. CX Codex +1 1,100installs neo4j-modeling-skill Designs, reviews and refactors Neo4j data models: nodes, relationships, properties, constraints. Useful once a graph schema starts to sprawl. CX Codex +1 1,100installs neo4j-driver-python-skill Tells your agent how to write Python against the Neo4j v6 driver: driver lifecycle, transactions, async, UNWIND batching. CX Codex +1 976installs neo4j-vector-index-skill A skill for building and querying Neo4j vector indexes: index creation, nearest-neighbor search, batch embedding updates. CX Codex +1 905installs neo4j-getting-started-skill Walks you from an empty folder to a running Neo4j app in eight stages, from tool checks to a notebook or service. CX Codex +1 888installs neo4j-gds-skill Guides graph projections, algorithms, and pipelines for the embedded Neo4j GDS plugin. Useful for Python and Cypher graph analytics workflows. CX Codex +1 865installs neo4j-security-skill This skill walks an agent through Neo4j user, role and privilege management in Cypher against the system database. Useful when access has to be provisioned by script instead of by hand. CX Codex +1 824installs

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor