Query performanceadvancedPro runbook

Read the loops multiplier in EXPLAIN ANALYZE

Inner node timings in EXPLAIN ANALYZE are per loop, not totals. A step reporting 0.001 ms looks free and is not, because the plan ran it two thousand times — and the buffer count is where that hides.

Problem

What you're actually looking at

The symptom as it shows up on a real server.

EXPLAIN ANALYZE reports actual time and rows for the average execution of a node, not the sum across executions. Under a nested loop the inner side runs once per outer row, so the number you read has to be multiplied by loops before it means anything — and the node that dominates a query is routinely the one displaying the smallest number.

A Meridian join takes far longer than the sum of its visible node timings. The inner index scan reports a fraction of a millisecond and is dismissed as trivial. It ran once for every outer row, and the buffers it touched account for almost the entire cost of the query.

In plain English

EXPLAIN ANALYZE puts two numbers on every step: how long it took and how many rows it returned. What is easy to miss is that for steps inside a loop, those are per-run averages rather than totals. A nested loop join runs its inner step once for each row coming from the outer step, so a step that reads "0.001 ms" and "1 row" may actually have run two thousand times. The time you should compare against the query total is the reported time multiplied by the loop count, and the same applies to rows. The reliable giveaway is the buffer count, because that one is a genuine total: when a step reports a rounding error for time but thousands of buffers, the loops are where your query went.

ProCaptured evidence where the run produced it

Full runbook for this incident

The scenario above is free. What Pro unlocks is the fix: how to identify read the loops multiplier in explain analyze, the exact SQL to trace it, PostgreSQL 18 output for the steps we captured, the resolution path, and how to stop it recurring.
  • The full identify checklist — the exact signals that tell you it's this incident
  • Every diagnostic query; PostgreSQL 18 output is attached only to the steps we captured
  • The resolution path and the pitfalls that make it worse
  • Mitigation steps to stop it recurring, plus a verify-you're-done query

Connected

How this connects to the rest of the library

A live view of this page's real cross-references — what explains it, what fixes it, what to tune, and where to go next. Every link is an authored relationship, not a guess.

Open in the interactive map →