Skip to main content
RunBook Academy

PostgreSQLX · Query Planning, Indexes and Performance MethodPlanner

Reading an EXPLAIN plan

Intermediate⏱ ~30 min🧪 Lab requiredpsql

What you'll learn

  • Read a plan tree in execution order rather than in printed order
  • Interpret loops correctly and compute the true cost of a nested node
  • Identify the node responsible for a plan being slow
  • Use the EXPLAIN options that answer specific questions

Prerequisites

Practice

Verified against PostgreSQL 18.x · PostgreSQL (comparison targets) 17.11, 16.15 · PostgreSQL (support calendar) 18, 17, 16, 15, 14 supported · pgBackRest 2.59.1 · PgBouncer 1.25.2 · Patroni 4.1.5 · Ubuntu (host baseline) 26.04 LTS · 2026-08-27

Not yet marked complete on this device.

EXPLAIN prints a tree with the root at the top. Execution goes the other way: leaves first, each node feeding its parent. Reading it in printed order is the most common reason people look at a plan and take nothing from it.

The shape

 Aggregate  (cost=56.57..56.58 rows=1 width=8) (actual time=0.146..0.146 rows=1.00 loops=1)
   Buffers: shared hit=3 read=3
   ->  Index Only Scan using ordersx_customer_idx on ordersx
         (cost=0.43..51.67 rows=1962 width=0) (actual time=0.010..0.088 rows=2010.00 loops=1)
         Heap Fetches: 0
         Buffers: shared hit=3 read=3

The Index Only Scan runs first and hands its rows to the Aggregate. Indentation is depth: a node’s children are the more-indented -> entries directly beneath it.

Read from the deepest, last-listed node upward. That is execution order.

Every number on a node line

(cost=0.43..51.67 rows=1962 width=0) (actual time=0.010..0.088 rows=2010.00 loops=1)
FieldMeaning
cost=0.43..51.67Estimated startup cost .. estimated total cost
rows=1962Estimated rows this node will produce
width=0Estimated average row width in bytes
actual time=0.010..0.088Real milliseconds to first row .. to last row
rows=2010.00Actual rows produced, per loop
loops=1How many times this node was executed

The pairing to internalise is rows=1962 against rows=2010.00. That is the estimate against reality, and lesson X-04 is entirely about it.

The options worth using

EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS, WAL, FORMAT TEXT) SELECT …;
OptionGives you
ANALYZEActually runs it, adds actual rows and timing
BUFFERSPages hit, read, dirtied and written per node
VERBOSEOutput column lists, schema-qualified names
SETTINGSAny planner setting that differs from its default
WALWAL generated, for INSERT/UPDATE/DELETE
COSTS OFFSuppresses estimates — useful for stable test output
TIMING OFFSuppresses per-node timing, reducing measurement overhead
FORMAT JSONMachine-readable, for tooling

BUFFERS should be automatic. Timing varies with cache state and system load; buffer counts do not. A query reading 16,667 buffers is doing more work than one reading 6, on any hardware, at any time of day.

SETTINGS is the one nobody uses and should. It reveals that the session you are debugging has enable_seqscan = off or a modified work_mem — which explains a plan you could not otherwise account for.

Node types you will meet

Scans. Seq Scan, Index Scan, Index Only Scan, Bitmap Heap Scan, Bitmap Index Scan, CTE Scan, Function Scan, Values Scan.

Joins. Nested Loop, Hash Join (with a Hash child that builds the table), Merge Join (with Sort children unless input is already ordered).

Aggregation. Aggregate (one result), GroupAggregate (needs sorted input), HashAggregate (builds a hash table, no sort needed).

Ordering and limiting. Sort with Sort Method and Memory or Disk — a Sort Method: external merge Disk: 84320kB line is a work_mem problem, covered in Part XI. Limit, Incremental Sort.

Parallelism. Gather and Gather Merge collect from workers. Anything below them named Parallel … runs in each worker.

Other. Materialize caches a subtree’s output for reuse. Memoize caches lookups on the inner side of a nested loop — a node whose Hits and Misses counts tell you whether it was worth it. Subquery Scan, Append and Merge Append for partitions and unions.

Finding the expensive node

A procedure that works:

  1. Read Buffers at the top. That is the total work. If it is small, the query is not doing much I/O and the problem is elsewhere.
  2. Find the node with the largest Buffers. Buffer counts are cumulative up the tree, so the node whose count is much larger than its parent’s other children is where the work happened.
  3. Check its loops. A modest node executed thousands of times is the commonest cause.
  4. Compare its estimate to its actual. If they diverge, the problem is upstream — statistics, not execution.
  5. Only then look at timing. Time is a consequence; buffers and row counts are causes.

What to take from this

  • Read bottom up and inside out. Indentation is depth.
  • loops > 1 means actual rows and actual time are per iteration. Multiply.
  • Buffers is cumulative and unaffected by clock overhead. It is the most trustworthy number in the plan.
  • Always pass BUFFERS. Consider SETTINGS when a plan makes no sense.
  • On parallel plans, loops=3 is the leader plus two workers, and Workers Launched may be lower than Workers Planned.
  • TIMING OFF removes measurement overhead while keeping row counts.

Cross-course references

  • Observability for Production Sysadmins — Part CII (Slow queries) covers getting from a latency series to the individual statement whose plan you then read, which is the step before this one.
  • Linux for Production Sysadmins — Part LXXIX (Troubleshooting methodology) covers reading evidence bottom-up, which is exactly how a plan tree is read.

Quiz

Knowledge check · 6 questions

  1. Q1. A plan node reads 'Bitmap Heap Scan (actual time=0.061..0.276 rows=30.00 loops=1000)' with 'Buffers: shared hit=6082 read=26918'. How much work did this node do?

  2. Q2. A colleague cannot reproduce a plan you saw in a support session, on the same data and the same server. Which EXPLAIN option is most likely to explain the difference?

  3. Q3. A parallel plan shows a Parallel Seq Scan with 'actual rows=670.00 loops=3' beneath a Gather. How many rows did that scan actually produce?

  4. Q4. Which statements about EXPLAIN output are correct? Select all that apply.

  5. Q5. Execution Time reported by EXPLAIN ANALYZE can meaningfully exceed the query's real duration, because instrumenting every node costs two clock reads per row.

  6. Q6. Describe the procedure you would follow to find the expensive node in an unfamiliar plan.

Passing score: 75%. Answers are checked in this browser.