SQL EXPLAIN ANALYZE Deep Dive
How to read EXPLAIN ANALYZE output, interpret cost estimates, spot bad plans, and use it to drive real performance improvements in production databases.
What you'll learn
- ✓What EXPLAIN vs EXPLAIN ANALYZE actually do
- ✓How to read node types and costs
- ✓Spotting bad estimates and missing indexes
- ✓Buffers, loops, and actual vs planned rows
- ✓Turning plans into fixes
Prerequisites
- •Basic SQL and indexes
What and Why
EXPLAIN shows the planner’s chosen execution plan for a query. EXPLAIN ANALYZE actually runs the query and reports real timings and row counts alongside the estimates. That difference - estimated vs actual - is where most performance wins hide.
If you only ever read query text, you are guessing. The plan tells you what the database is really doing: which indexes it picked, how it joined tables, and where the time went. Once you can read a plan, tuning becomes engineering rather than superstition.
Mental Model
A plan is a tree of nodes. Each node consumes rows from its children and emits rows to its parent. The root is the final result. Costs are unitless and only meaningful relative to each other. Always compare rows= estimates against actual rows= - a 100x mismatch usually means stale statistics or a correlated predicate.
Hands-on Example
Consider a slow query joining orders and customers:
EXPLAIN (ANALYZE, BUFFERS)
SELECT c.name, SUM(o.total)
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.created_at >= '2026-01-01'
GROUP BY c.name;
HashAggregate (cost=12450..12500 rows=50 width=40)
(actual time=480..481 rows=48 loops=1)
-> Hash Join (cost=210..12100 rows=70000 width=36)
(actual time=4..420 rows=68234 loops=1)
Hash Cond: (o.customer_id = c.id)
-> Seq Scan on orders o (rows=70000)
(actual rows=68234)
Filter: (created_at >= '2026-01-01')
Rows Removed by Filter: 930000
-> Hash (rows=500)
-> Seq Scan on customers c The Seq Scan on orders removed 930k rows by filter - a perfect candidate for an index on created_at. After adding the index:
CREATE INDEX idx_orders_created_at ON orders(created_at);
You should see an Index Scan or Bitmap Index Scan replacing the Seq Scan, and actual time on that node should drop dramatically.
Common Pitfalls
The most common mistake is reading cost as milliseconds. It is not. The planner uses cost to compare alternatives; only actual time is real wall-clock.
Another trap is forgetting the loops multiplier. A nested-loop inner side shows time per loop; total time is actual time * loops. A 0.05 ms node executed 200,000 times is 10 seconds.
People also run EXPLAIN ANALYZE on INSERT, UPDATE, or DELETE in production and accidentally mutate data. Wrap it in a transaction and roll back:
BEGIN;
EXPLAIN ANALYZE UPDATE ... ;
ROLLBACK;
Finally, do not chase a single slow query while ignoring Buffers. A plan that hits disk hard (read=) on a warm system may be fine after caching - or may signal that the working set no longer fits in memory.
Practical Tips
Always use EXPLAIN (ANALYZE, BUFFERS, VERBOSE) in Postgres. BUFFERS shows shared hit vs read - the truth about cache behavior. VERBOSE shows output columns, which helps when a CTE or subquery is unexpectedly materialized.
When estimates are wildly off, run ANALYZE table_name to refresh statistics. For skewed columns, raise default_statistics_target or set per-column statistics.
Look for these red flags in order: Seq Scan on large tables with selective filters, Nested Loop with large outer side, Sort spilling to disk (Sort Method: external merge), and Hash Join with Batches: > 1 indicating memory pressure.
For complex plans, paste the output into a visualizer like explain.dalibo.com or explain.depesz.com. They highlight the expensive nodes and make the tree navigable.
Save plans before and after every change. A performance fix without a before/after plan is a story without evidence.
Wrap-up
EXPLAIN ANALYZE is the single most important debugging tool for SQL performance. Read the tree bottom-up, compare estimated to actual rows, multiply by loops, and check buffers. Most slow queries reveal themselves the moment you look at the plan instead of the query text.
Make plan-reading part of code review for any query touching a large table, and your database will reward you with predictable latency.
Related articles
- SQL SQL EXPLAIN and Query Planning Made Approachable
Learn how to read EXPLAIN output, what scan and join types mean, and how to spot the indexes and rewrites that make slow queries fast.
- SQL CTE vs Subquery vs Temp Table: Picking the Right SQL Tool
Compare common table expressions, subqueries, and temporary tables in SQL. Learn when each shines, the performance trade-offs, and concrete examples.
- SQL SQL Indexes: B-Tree vs Hash vs GIN
A practical comparison of B-Tree, Hash, and GIN indexes in PostgreSQL, when each one shines, and how to pick the right structure for your query patterns.
- SQL SQL Materialized Views: A Practical Tutorial
Learn how materialized views work in PostgreSQL, when to use them over regular views, and how to refresh them efficiently without blocking readers.