explain-plan-reader
Skill0 starsChanged 39 days agoMITView on GitHub
Read a captured EXPLAIN ANALYZE plan, name the single bottleneck node, and prescribe a targeted fix for it. Use when a specific query is slow and you have (or can capture) its execution plan from Postgres or MySQL and need to know WHICH node is burning the time and why.
In this repository7 other files it gives its agents.
- connection-pool-tunerSkill
- index-advisorSkill
- migration-safety-checkerSkill
- n-plus-one-hunterSkill
- partition-plannerSkill
- query-rewriterSkill
- replication-lag-debuggerSkill
The file
We split this skill into the rules it gives an agent
6 rules. About 0 are common, 4 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 32
Every diagnosis cites a number FROM THE PLAN (actual time, loops, rows estimated vs actual, batches, buffer reads).
- Distinctive: line 38
Do NOT tune nodes consuming under ~5% of total time, or flag a node for its scary name alone - a Seq Scan on a 50-row lookup table is …
- Distinctive: line 39
Do NOT design the index (column order, partial/covering, included columns).
- Distinctive: line 40
Do NOT use this when the symptom is many similar queries fired in a loop from application/ORM code (the plan of any single one looks fine) …
- Very specific: line 41
Do NOT prescribe from plain EXPLAIN estimates or from query text.
- Very specific: line 42
Do NOT raise work_mem globally as a reflex - it is allocated per sort/hash node per connection, so a global bump can exhaust memory under …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
EXPLAIN Plan Reader
Turn one captured execution plan into a named bottleneck node and a specific fix. A plan is a tree executed bottom-up and inside-out; your job is to find the one node where time or rows explode, not to audit every line.
Workflow
- Demand a real plan first. Do not diagnose from query text alone. In Postgres, require
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)- ANALYZE runs the query for real timings, BUFFERS shows cache-vs-disk reads. In MySQL, requireEXPLAIN ANALYZE. PlainEXPLAINshows estimates only and lies under data skew; if that is all you have, say so and ask for ANALYZE output before prescribing. - Run it twice. The first run can be cold cache. Trust the warm run's timings unless cold-cache I/O is itself the problem (confirm via BUFFERS: high
read=vshit=). - Find the bottleneck node. Postgres reports cumulative
actual time; a node's own cost is its time minus its children's. Locate the node with the largest delta or the largestloops- a real bottleneck typically owns 50% or more of total runtime; if no single node does, the time is spread and the query shape (not one node) is the problem.(actual rows=R loops=N)means the node ran N times - multiply R×N for true rows. A Nested Loop with highloopsdriving a Seq Scan is the classic disaster. In MySQL, scan fortype=ALL(full scan), largerowsexamined, andExtra: Using filesort/Using temporary. - Confirm with the estimate gap. Compare estimated vs actual rows on the hot node. A gap of 10x or more (e.g. estimated 1, actual 500000) means the planner is flying blind - that misestimate usually causes the bad node choice upstream and is the root cause, not the node itself. A gap under 2-3x is normal noise; do not chase it.
- Diagnose by node type and prescribe one fix:
- Seq Scan on a large table filtered to few rows → the predicate is not served by an index. Name the column(s) and predicate shape; check whether the WHERE wraps the column in a function (defeats a plain index). Hand the access pattern to index-advisor for the actual index choice.
- Bad row estimate → stale or insufficient statistics. Run
ANALYZE the_table; for skewed columns raisedefault_statistics_target(default 100) to 500-1000 for the affected column and re-ANALYZE. - External merge / disk-spilling Sort → raise
work_memfor the session (the Postgres default is a conservative 4MB; sorts spill the moment they exceed it), or provide pre-sorted input via an index ordering. - Hash Join with
Batches > 1(spilling to disk) → raisework_mem. - Nested Loop chosen for a large set → almost always a row underestimate upstream; fix the estimate (step 4) and the planner switches to Hash/Merge Join on its own.
- MySQL
Using filesortwith largerows→ unindexed ORDER BY; provide an index covering the sort order.
- State the verdict in one line: the node, its share of total time, the root cause, and the single highest-leverage fix.
Deliverable
Produce a one-page diagnosis containing: the verdict line (bottleneck node, its percentage of total runtime, root cause, single fix), the supporting numbers quoted directly from the plan (actual time, loops, estimated vs actual rows, batches or buffer reads as relevant), the prescribed fix as a runnable statement or setting change, and the instruction to re-capture the plan after applying it.
Quality bar
- Every diagnosis cites a number FROM THE PLAN (actual time, loops, rows estimated vs actual, batches, buffer reads). No folklore fixes.
- Exactly one bottleneck named per pass. If a fix shifts the bottleneck, ask for a fresh plan and repeat.
- Trust
actual time, never the estimatedcost=units (they are not milliseconds), once ANALYZE output is available.
Do NOT
- Do NOT tune nodes consuming under ~5% of total time, or flag a node for its scary name alone - a Seq Scan on a 50-row lookup table is correct and fast.
- Do NOT design the index (column order, partial/covering, included columns). Name the access pattern and defer the index design to index-advisor.
- Do NOT use this when the symptom is many similar queries fired in a loop from application/ORM code (the plan of any single one looks fine) - that is an N+1 pattern; use n-plus-one-hunter instead.
- Do NOT prescribe from plain
EXPLAINestimates or from query text; require an ANALYZE-captured plan first. - Do NOT raise
work_memglobally as a reflex - it is allocated per sort/hash node per connection, so a global bump can exhaust memory under concurrency; scope it to the session or query first.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

