Read it inside out, bottom-up, and compare two numbers per node: rows estimated versus rows actual. A plan is a tree; the deepest nodes run first and hand their rows upward.
EXPLAIN alone prints the planner's guess. EXPLAIN (ANALYZE, BUFFERS) actually runs the query and prints what happened, including how many blocks came from cache versus disk. Because it runs, wrap it in a transaction you roll back before pointing it at an UPDATE.
The estimate gap is the tell. A node that expected 12 rows and got 400,000 did not just misreport itself: the planner chose the join strategy above it based on that 12, so a nested loop is now running 400,000 times.
Estimated vs actual rowsthe gap explains almost every bad plan
- ↓
actual time and loopsper-loop time multiplies by loops
- ↓
Rows Removed by Filterwork done to throw work away
- ↓
Sort Methodexternal merge Disk means work_mem is too small
Costs are unitless and only useful for comparing two candidate plans. Wall-clock time on the node, and the row gap that caused it, are what you act on.