EXPLAIN and EXPLAIN ANALYZE in PostgreSQL: Reading a Query Plan
Learn how to read PostgreSQL's EXPLAIN and EXPLAIN ANALYZE output — Seq Scan vs Index Scan, cost estimates, actual execution time, and how to spot a slow query.
EXPLAIN and EXPLAIN ANALYZE
EXPLAIN shows you how PostgreSQL intends to execute a query, without actually running it — invaluable for figuring out why a query is slow.
EXPLAIN — The Plan Only
EXPLAIN SELECT * FROM employees WHERE department = 'Engineering';
Seq Scan on employees (cost=0.00..25.88 rows=6 width=64)
Filter: (department = 'Engineering'::text)
- Seq Scan — reads every row in the table (slow on large tables).
- cost=0.00..25.88 — Postgres's internal cost estimate (startup cost .. total cost), not real time.
- rows=6 — the estimated number of matching rows.
EXPLAIN ANALYZE — Actually Run It
EXPLAIN ANALYZE executes the query for real and reports actual timing alongside the estimates — use it to confirm whether the plan matches reality, and to see real duration.
EXPLAIN ANALYZE SELECT * FROM employees WHERE department = 'Engineering';
Seq Scan on employees (cost=0.00..25.88 rows=6 width=64) (actual time=0.02..0.15 rows=6 loops=1)
Filter: (department = 'Engineering'::text)
Planning Time: 0.08 ms
Execution Time: 0.20 ms
Because it really runs the query, be careful running EXPLAIN ANALYZE on a destructive statement (UPDATE/DELETE) in production — the change genuinely happens.
Advertisement728 x 90
After Adding an Index
CREATE INDEX idx_department ON employees (department);
EXPLAIN ANALYZE SELECT * FROM employees WHERE department = 'Engineering';
Index Scan using idx_department on employees (cost=0.28..8.45 rows=6 width=64) (actual time=0.02..0.03 rows=6 loops=1)
Index Cond: (department = 'Engineering'::text)
Execution Time: 0.05 ms
Seq Scan became Index Scan, and execution time dropped — direct proof the index is helping.
Things to Watch For
- Seq Scan on a large table for a selective filter usually means a missing index.
- A big gap between estimated rows and actual rows suggests Postgres's statistics are stale — run
ANALYZE table_name;to refresh them. EXPLAIN (ANALYZE, BUFFERS)also shows how much data came from cache vs. disk — useful for deeper performance investigations.
Common Errors & Troubleshooting
- Running
EXPLAIN ANALYZEon a slowUPDATE/DELETEin production — it actually executes the statement; if you just want the plan without side effects, use plainEXPLAINinstead. - Estimated rows wildly different from actual rows — a big gap here signals stale table statistics; run
ANALYZE tablename;and re-check. - Misreading
costas milliseconds — thecost=X..Ynumbers are arbitrary planner units, not real time; onlyEXPLAIN ANALYZE'sactual time=...andExecution Time:lines are real, measured durations.
Key Takeaways
EXPLAINshows the plan;EXPLAIN ANALYZEactually runs the query and reports real timing too.Seq Scan→Index Scanis the classic sign an index fixed a slow query.- Run plain
ANALYZE table_name;periodically so Postgres's row-count estimates stay accurate.
Advertisement320 x 100