Workload-level tuning starts with a ranked list of offenders and drowns in it. Pull the top queries from pg_stat_statements or sp_BlitzCache, add an LLM annotation per query, then do the human prune — accept, reject, needs-testing — that separates real signal from confident noise.
Tier: Core · Workflow area: SQL Authoring & Query Tuning · ~35 minutes
Single-query tuning (pb-03) assumes you know which query matters. This playbook finds it. The pattern comes from sp_BlitzCache's @AI-style annotation: machine-rank the workload, machine-annotate each offender, human-prune the annotations. The prune is not optional — it is the step that makes the sheet trustworthy.
Pull the top offenders with real numbers. Rank by total time (what the server feels) and by mean time (what a user feels) — they surface different problems:
-- PostgreSQL: pg_stat_statements, top 15 by total time
SELECT queryid,
left(query, 80) AS query_start,
calls,
round(total_exec_time::numeric/1000, 1) AS total_s,
round(mean_exec_time::numeric, 2) AS mean_ms,
rows,
round(100.0 * shared_blks_hit /
nullif(shared_blks_hit + shared_blks_read, 0), 1) AS hit_pct
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 15;
On SQL Server the equivalent single move is sp_BlitzCache @SortOrder = 'cpu' (then 'reads'), or Query Store's top-resource-consumers view.
Annotate the batch with one structured prompt. Paste the top-N export and ask for a per-row annotation, not an essay:
You are triaging a PostgreSQL 16 workload. Below: the top 15 queries
from pg_stat_statements with calls, total time, mean time, rows and
cache hit ratio, plus the normalized query text.
For EACH query return one row:
- likely_issue: one line (e.g. "N+1 pattern: 2.1M calls, 0.4ms mean",
"low hit ratio: reads working set from disk", "sort spill suspect")
- evidence: which numbers from MY data support that reading
- next_check: the ONE diagnostic to run before any fix
- candidate_fix: one line, marked [rewrite] [index] [app] or [config]
- confidence: high / medium / low
Do not propose fixes for queries whose numbers look healthy; write
"leave alone" instead.
Prune every row — this is the human step. Go down the sheet and mark each annotation accept / reject / needs-testing, with a one-line reason. A pruned sheet looks like:
queryid | AI annotation | verdict | reason
8841002... | N+1: 2.1M calls at 0.4ms | accept | matches ORM lazy-load in app repo
1290583... | missing index on status col | reject | partial idx exists; AI can't see
| | | planner chose seq scan on purpose
5520091... | sort spill, raise work_mem | needs-testing | plausible; check log for temp files
7018334... | "leave alone" | accept | agreed, healthy numbers
Expect to reject 20–40% of annotations. That is the system working: the model reads numbers well but cannot see your partial indexes, your intentional design choices, or last week's incident.
Run the next_check for everything marked needs-testing. Example — confirming the sort-spill theory before touching work_mem:
SELECT queryid, temp_blks_written
FROM pg_stat_statements
WHERE temp_blks_written > 0
ORDER BY temp_blks_written DESC LIMIT 5;
Evidence found: promote to accept. Not found: reject, note why. Never promote on plausibility alone.
Implement exactly one fix, measured. Take the highest-value accepted row into the pb-03 loop: snapshot the query's pg_stat_statements numbers, apply the fix on a copy, verify, ship, then compare a same-length window:
SELECT pg_stat_statements_reset(); -- on the copy, for a clean window
-- ... replay or wait one business cycle, then re-pull the top-15
One measured improvement per pass builds more credibility than five unmeasured ones.
Keep the sheet as a living artifact. Date it, file it with the tuning tickets, and re-run the pass monthly. The diff between passes — offenders that left the list, new arrivals — is your workload's changelog.
sp_BlitzCache @SortOrder='cpu' then @SortOrder='reads', export to the sheet, same annotation prompt with the columns renamed; Query Store gives you the regression view (sys.query_store_runtime_stats) for the before/after, and plan forcing is your emergency brake, not your fix.performance_schema.events_statements_summary_by_digest (SUM_TIMER_WAIT, COUNT_STAR, SUM_ROWS_EXAMINED); the rows-examined-to-rows-sent ratio is your strongest annotation input, and pt-query-digest over the slow log is the fallback where performance_schema is trimmed.V$SQLSTATS and use SQL Monitor reports for the deep dive on any accepted row.pg_stat_statements_reset() destroys your baseline. Snapshot the current numbers first; on shared instances, coordinate before resetting.Normalized query text can still embed literals and table names that reveal business logic. Review the export before it leaves your environment, and prefer a tenant-approved model for full workload dumps.
The ranked list finds where the time goes; the LLM annotation guesses why; the human prune decides what is real. Skip the prune and you are executing a language model's hunches against your busiest queries.