Lesson 0006 · The Planner

Reading EXPLAIN

You already read MySQL plans. Postgres shows a tree of plan nodes with costs and row estimates — and one comparison (estimated vs actual rows) tells you more than all the rest.

~11 minWarm-up recall + quiz + MySQL contrastCheat sheet: here

Warm-up: recall 0003 & 0005 first

Closed book. Both show up the moment you read a real plan.

In an Index Only Scan, a non-zero Heap Fetches count means:

Heap Fetches count the rows where the visibility-map bit wasn't set, so the scan had to visit the heap anyway. Vacuum the table to set more all-visible bits and drive it toward zero.

Postgres's default isolation level is:

Read Committed — a fresh snapshot per statement. (InnoDB defaults to Repeatable Read; that silent flip is the migration trap from 0005.)

In MySQL you read a plan as a row per table: access type, key, rows, filtered, Extra (that was MySQL lesson 0003). Postgres shows something different: a tree of plan nodes, each annotated with a cost and a row estimate. You read it inside-out / bottom-up — the most-indented nodes run first and feed their parents.

Anatomy of one node

Seq Scan on orders (cost=0.00..18.50 rows=5 width=36) │ │ │ │ │ └─ estimated avg bytes per output row │ └─ estimated rows this node EMITS (after its filter) └─ startup_cost .. total_cost (arbitrary units; includes children)

Three things to internalize about that line:

EXPLAIN vs EXPLAIN ANALYZE

Plain EXPLAIN only estimates — it does not run the query. Add ANALYZE and Postgres actually executes it, then shows the real numbers next to the estimates. PostgreSQL Docs: "With this option, EXPLAIN actually executes the query, and then displays the true row counts and true run time accumulated within each plan node, along with the same estimates that a plain EXPLAIN shows."

ANALYZE runs your query — including writes

EXPLAIN ANALYZE UPDATE … performs the UPDATE. The results are discarded but the side effects are real. PostgreSQL Docs: "because EXPLAIN ANALYZE actually runs the query, any side-effects will happen as usual… If you want to analyze a data-modifying query without changing your tables, you can roll the command back." So the safe habit for DML is BEGIN; EXPLAIN ANALYZE UPDATE …; ROLLBACK;.

The one skill that matters most

When you read an EXPLAIN ANALYZE, the first thing to check is whether the estimated rows is close to the actual rows. PostgreSQL Docs: "The thing that's usually most important to look for is whether the estimated row counts are reasonably close to reality." A node that estimates rows=5 but actually emits rows=50000 means the planner was flying blind — and a wrong row estimate cascades into wrong join and scan choices above it. Those estimates come from table statistics gathered by ANALYZE — and remember from lesson 0002 that autovacuum runs ANALYZE too. PostgreSQL Docs: autovacuum automates "VACUUM and ANALYZE commands." So a wildly-off estimate often just means stale statistics → run ANALYZE thetable; and look again.

BUFFERS: where the time really went

Add BUFFERS (implicitly on whenever you use ANALYZE in recent versions) to see cache vs disk: PostgreSQL Docs: "The ANALYZE option implicitly enables the BUFFERS option," which shows "buffers hit, read, dirtied, and written."

Buffers: shared hit=36 read=6 │ └─ blocks fetched from disk (misses — the slow part) └─ blocks served from the cache (buffer pool)

Two plans with the same cost can behave very differently at 3 a.m. when the cache is cold: read= is the number that hurts. This is your window into the buffer pool — the Postgres cousin of InnoDB's.

The scan nodes you'll actually see

Everything from the storage lessons shows up here by name:

Postgres vs MySQL — same job, different readout

QuestionMySQL / InnoDBPostgreSQL
Plan shapeOne row per table accessedA tree of plan nodes (read bottom-up)
Cost readoutrows × filtered heuristicscost=startup..total (page-fetch units)
"How many rows"rows examined estimaterows = estimated rows emitted
Actually run itEXPLAIN ANALYZE (8.0+)EXPLAIN ANALYZE (executes; wrap DML)
Cache visibilityBUFFERS: shared hit vs read
Top diagnosticrows × filtered vs actualestimated vs actual rows

Check yourself

From memory. Two items reach back on purpose.

In (cost=0.00..18.50 rows=5 width=36), the rows=5 is:

Plain EXPLAIN shows the planner's estimate of rows emitted after filtering — not scanned, and not actual. Actual counts only appear with ANALYZE.

Reading EXPLAIN ANALYZE, the single most useful check is:

A big gap between estimated and actual rows means the planner was working from bad numbers, which cascades into poor join and scan choices. It's the first thing to look at.

Estimated rows are wildly off from actual. The usual first fix is:

Estimates come from table statistics gathered by ANALYZE (which autovacuum also runs). Stale stats are the common cause of bad estimates — refresh them first.

EXPLAIN ANALYZE UPDATE t SET … is dangerous because it:

ANALYZE executes the statement, so the UPDATE really happens. Wrap it in BEGIN … ROLLBACK to inspect the plan without changing data.

In Buffers: shared hit=36 read=6, the number that signals slow I/O is: (recall the storage arc)

"hit" came from the cache (fast); "read" missed the cache and went to disk (slow). A plan that looks cheap can still be slow if read is high on a cold cache.
Optional — when you have an instance

A lab to run later. It shows estimate-vs-actual and the safe-DML trick:

CREATE TABLE t (id int PRIMARY KEY, k int);
INSERT INTO t SELECT g, g % 50 FROM generate_series(1,100000) g;

EXPLAIN SELECT * FROM t WHERE k = 7;               -- estimates only, no run
EXPLAIN (ANALYZE) SELECT * FROM t WHERE k = 7;     -- compare rows vs actual rows; note Buffers
ANALYZE t;                                         -- refresh stats
EXPLAIN (ANALYZE) SELECT * FROM t WHERE k = 7;     -- estimate should tighten

BEGIN; EXPLAIN ANALYZE UPDATE t SET k = k + 1 WHERE id = 1; ROLLBACK;  -- inspect DML safely

Bring a plan where estimated and actual diverge and we'll work out why together.

Primary source — read this next

PostgreSQL Docs — 14.1 Using EXPLAIN (the whole node-by-node walkthrough, cost units, ANALYZE, BUFFERS, and the estimate-vs-reality advice). Keep this page open the first few times you read a real plan.