Systematic Query Profiling

A repeatable method for finding what actually makes a query slow — one hypothesis, one change, one controlled re-measurement at a time — instead of changing several things at once and hoping.

Everything earlier in this module explains what a plan is doing. This article is about the discipline of finding which part actually needs fixing — the scientific method applied to a slow query, rather than a list of remembered fixes tried in whatever order comes to mind.

Bottlenecks are not all the same kind of problem#

A slow query can be slow for genuinely different reasons, each needing a different fix:

Text
I/O-bound:     waiting on disk reads or network transfer of dataCPU-bound:     heavy computation — complex expressions, large sorts,               aggregations over many rowsLock-bound:    waiting for another transaction to release a lock               on the same rows or tableNetwork-bound: the result set itself is large and slow to transfer               to the client, independent of how fast it was computed

Applying an I/O fix — adding an index — to a lock-contention problem does nothing, because the query was never waiting on disk in the first place. Identifying which category a slow query actually falls into comes before choosing what to change, not after.

A repeatable method#

  1. Establish a baseline
    Run the query under realistic conditions and record its actual time — not a guess, a measured number to compare every later change against.
  2. Get the real plan
    EXPLAIN ANALYZE (or the equivalent), not plain EXPLAIN — Reading Query Execution Plans covers finding the single node that actually dominates total time.
  3. Form one hypothesis
    Missing index? Stale statistics? A function wrapped around the filtered column? Pick the most likely explanation for that specific node's cost — not a general sense that "the query is slow."
  4. Make exactly one change
    Add the index, refresh statistics, rewrite the one filter — and nothing else at the same time.
  5. Re-measure under the same conditions
    Same data, same load, same cache state as the baseline. Compare the new number against the recorded one.
  6. Keep or revert, then repeat
    If the change helped, keep it and move to the next-largest node if the query is still too slow. If it didn't, revert it — a change that doesn't help is not harmless just because it also didn't hurt.

The trap inside "re-measure under the same conditions"#

A query run immediately after a previous run of the same query is often faster the second time — not because anything was fixed, but because the relevant data is now sitting in a cache from the first run. Comparing a cold-cache baseline against a warm-cache "improved" measurement credits the cache, not the actual change — cache state is a confounding variable here, exactly the kind Statistical Pitfalls in Everyday Data Analysis warns about, and the apparent fix evaporates the next time the query runs cold in production.

Control for the cache, or the comparison is meaningless

Either measure every run cold (clearing or bypassing relevant caches between runs) or measure every run warm (running once to warm the cache, then taking the timing on a second run, every single time) — consistently, for both the baseline and every change afterward. Mixing the two makes every comparison in the whole process unreliable.

Why one change at a time, not several#

Changing an index, rewriting a filter, and refreshing statistics all in one pass might genuinely make a query faster — and leaves no way to know which of the three changes actually mattered, or whether one of them was quietly making things worse while another compensated for it. The next time a similar problem appears, none of that combined experiment's insight transfers, because it was never isolated in the first place. This is the same discipline Testing Ingestion Code Without Hitting the Network applies to verifying a retry loop deliberately, one failure mode at a time — an isolated, controlled change is the only kind whose effect is actually knowable.

Common mistakes#

Judging a change from a single fast run#

Query timing has natural variance — one run finishing quickly can be coincidence, not a real improvement. Running a change several times and looking at a consistent pattern, not one lucky number, is what actually confirms it worked.

Profiling against a dataset much smaller than production#

A query that is fast against a thousand-row development table can behave completely differently at production scale — plan choices themselves can change as data grows, not just their absolute speed. Profiling against data of a comparable size and shape to production is the only way the result actually transfers.

Changing several things and attributing the result to a guess#

Without isolating one change at a time, a genuine improvement and an unrelated coincidence look identical. The discipline in this article exists specifically to prevent that ambiguity from accumulating across a long tuning session.

Further reading#

See also#