Query plan analyzer
Skill imtiazrayhan/agentscamp-library/skills/query-plan-analyzer
Read a slow query's execution plan and turn it into a concrete fix — the exact index to add, the rewrite, or the ANALYZE to run — by getting the REAL plan with EXPLAIN ANALYZE (actual rows + timing, not estimates), finding the offending node, and confirming the fix removes it. Use when one specific query is slow and you need to know WHY, not just that it is.From its SKILL.md
npx -y skills add imtiazrayhan/agentscamp-library --skill query-plan-analyzerAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
8.1 KB, ~1.9k tokens by cl100k_base, as published. Nobody here has run it
A slow query is almost never slow for the reason you'd guess from reading the SQL. The plan is the ground truth: it shows the database actually chose a Seq Scan over the 40-million-row table, actually fed 500,000 rows into a Nested Loop that estimated 5, actually sorted on disk because no index could supply the order. This skill pulls the real plan — EXPLAIN ANALYZE with BUFFERS, not bare EXPLAIN — reads it from the most expensive node outward, names the one node that's costing the time and why, and turns that into a specific fix: the index to add (with the right column order), the rewrite that makes the predicate sargable, or the ANALYZE that fixes the estimate. Then it re-runs the plan to prove the bad node is gone instead of declaring victory from theory.
When to use this skill
- One specific query (an endpoint, a report, a dashboard panel) is slow and you need the cause, not a vague "add some indexes."
- A query that was fast got slow after a data-volume change, a deploy, or a schema/index change.
- The planner is doing something surprising — a Seq Scan despite an index existing, or ignoring the index you just added.
- p99 latency on one query is high while the table and load look unremarkable, and you suspect the plan rather than the hardware.
- Before shipping a new query or a
migration-writerindex change, to verify the plan is what you intended.
Instructions
- Get the table shape and existing indexes before touching the plan. Read the schema for the queried tables: column types, the existing indexes and their column order, row counts (
SELECT reltuples FROM pg_class, or\d+), and whether stats are fresh (pg_stat_user_tables.last_analyze/n_mod_since_analyze). Grep the codebase for where the query is built so you tune the real SQL (including how parameters bind), not a hand-typed approximation. - Run the REAL plan with actual rows, timing, and I/O — never bare EXPLAIN. Use
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)in Postgres (ANALYZE FORMAT=TREE/EXPLAIN ANALYZEin MySQL 8+).ANALYZEexecutes the query and reports actual rows + per-nodeactual time;BUFFERSshows shared/local hits vs. reads (heavyread=means I/O, not CPU, is the cost). Run it 2–3 times so a cold-cache first run doesn't masquerade as a planning problem. For a write query, wrap it in a transaction andROLLBACKsoANALYZEdoesn't mutate data. - Read from the most expensive node outward — find where the time actually is. In the text plan,
actual time=start..endis cumulative and inclusive of children; the time a node adds is its end-time minus its children's. Find the deepest/innermost node whoseactual timeandloops × rowsdominate the total. That node — not the top of the plan — is what you fix. Note itsactual rows,loops, and theRows Removed by Filterline. - Check the estimate-vs-actual gap FIRST — a wide gap means stale stats, and that's the real bug. Compare each node's estimated rows (
rows=) toactual rows. A gap of more than ~10x (e.g. plans for 5 rows, processes 50,000) means the planner is choosing strategy on bad information — usually stale statistics. Fix this before adding any index: runANALYZE <table>;(orANALYZEthe whole DB) and re-pull the plan. Often the plan corrects itself once estimates are right, and an index you'd have added would have been the wrong one. - Match the symptom to the culprit, then to the fix:
- Seq Scan on a large table with a selective predicate → the predicate filters to few rows but there's no usable index. Add a b-tree on the filtered column(s). (A Seq Scan returning most of the table is correct — don't index it.)
- Nested Loop with high
loopsover many outer rows → the join is iterating per-row when it should batch. The cause is usually a bad row estimate (see step 4) or a missing join-key index; a corrected estimate or an index on the inner join column lets the planner pick a Hash/Merge Join. - Sort (especially
Sort Method: external merge Disk:) → the query sorts at runtime and spills to disk. A b-tree index in theORDER BYorder can supply rows pre-sorted, removing the Sort node entirely (and poweringLIMITearly-exit). - High
Rows Removed by Filter→ the database fetched far more rows than it kept; the filter ran after the scan instead of being pushed into an index. Move the discriminating column into the index so it's a condition, not a post-filter. - Heavy
Buffers: ... read=→ the working set isn't cached; a smaller/covering index reduces pages touched, or the data genuinely doesn't fit memory.
- Check index sargability — an index the predicate can't use is no fix at all. A b-tree is defeated by a function or cast on the column (
lower(email) = ?,date(created_at) = ?,col::text = ?), by a leading-wildcardLIKE '%x', and by anORacross different columns. The fix is a matching expression index (CREATE INDEX ... ON t (lower(email))), a rewrite to a range (created_at >= d AND created_at < d+1), orUNION-ing theORbranches — not a plain index on the raw column. - Order multi-column index columns for the predicate, then the sort. Put equality-predicate columns first (leftmost), then the range/inequality column, then
ORDER BYcolumns — so one index serves both the filter and the ordering. A column used only for a range can't have an equality column usefully placed after it. State the exactCREATE INDEXDDL, includingINCLUDEd columns if a covering index would turn an Index Scan into an Index-Only Scan. - Re-run
EXPLAIN ANALYZEafter the fix and confirm the bad node is gone. Apply the fix (in Postgres, build the indexCONCURRENTLYto avoid a write lock;migration-writercan wrap the DDL). Re-pull the plan and verify the offending node changed type (Seq Scan → Index Scan, Nested Loop → Hash Join, Sort → no Sort) and that totalactual timedropped. If the planner ignores the new index, runANALYZEand re-check sargability before concluding the index is wrong.
[!WARNING] Bare
EXPLAINshows the planner's guess, not reality — it never runs the query, so it can't reveal a Nested Loop that estimated 5 rows and processed half a million, or which node actually burned the time. Diagnose withEXPLAIN ANALYZEevery time; tuning from estimates is how you add the wrong index.
[!WARNING] A wide estimated-vs-actual row gap (>10x) means stale statistics, and that is the root cause — fix it with
ANALYZEbefore adding indexes. An index chosen to compensate for a bad estimate is often useless or harmful once the estimate is corrected, and you'll have shipped a write-amplifying index that the planner ignores.
[!NOTE]
EXPLAIN ANALYZEexecutes the statement. ForINSERT/UPDATE/DELETE, run it insideBEGIN; ... ROLLBACK;so diagnosis doesn't change data — and be aware it still fires triggers and acquires locks during the run.
Output
A short report with three parts:
- Annotated plan — the offending node quoted from the
EXPLAIN ANALYZEoutput, with itsactual rowsvs. estimate,loops,Rows Removed by Filter, andBuffers, plus a one-line statement of why it's the bottleneck (Seq Scan / stale-stats row gap / Nested Loop blowup / disk Sort / non-sargable predicate). - The specific fix — exact
CREATE INDEX ... CONCURRENTLYDDL with the column order justified, or the SQL rewrite, or theANALYZE <table>command. One concrete action, not a menu. - Before/after proof — total
actual timeand the changed node type from the re-run plan (e.g.Seq Scan 1240 ms → Index Scan 3 ms), confirming the bad node is gone rather than asserting it should be.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.