Overview

The planner (optimizer) turns a SQL statement into a plan: it enumerates candidate scan methods, join orders, and index choices, estimates each one’s cost from table statistics gathered by ANALYZE, and runs the cheapest. Read the plan before adding an index or rewriting SQL, because guesses about slowness are often wrong. When a query suddenly picks a bad plan, suspect the planner’s inputs (stale statistics, skewed data) before the planner.

Rules

  • EXPLAIN shows the estimated plan. EXPLAIN ANALYZE runs the query and adds actual rows, timing, and loops; from PostgreSQL 18 it includes BUFFERS by default, and earlier versions need EXPLAIN (ANALYZE, BUFFERS).
  • Compare estimated and actual row counts. A large gap means stale statistics (run ANALYZE; see vacuum) or correlated columns that need CREATE STATISTICS.
  • Key nodes: Seq Scan, Index Scan, Index Only Scan (needs a covering index and a current visibility map), Bitmap Heap Scan, and Nested Loop, Hash, and Merge joins.
  • Planner switches such as enable_seqscan = off are session-level debugging tools, never a production fix for a missing index (sequential-scan).
  • Many shared read buffers mean the data came from outside shared_buffers. A fast plan executed hundreds of times per request is a different problem: n-plus-one-query.
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, title FROM articles
WHERE author_id = 42 AND published_at > now() - interval '30 days'
ORDER BY published_at DESC;
-- A Seq Scan on a large table: try an index on (author_id, published_at DESC), then re-run.