What is the difference between EXPLAIN and EXPLAIN ANALYZE, and how do you read a query plan?
EXPLAIN shows the plan PostgreSQL intends to use, with estimated costs and row counts, without running the query. EXPLAIN ANALYZE actually runs the query and adds real timings and real row counts, so you can compare estimates with reality.
Explain it simply
Before an IRCTC train leaves, the chart shows the planned route and expected arrival times. That is EXPLAIN: the plan and the guesses. After the journey, the running status tells you the real times at every station and where it got delayed. That is EXPLAIN ANALYZE: the plan plus what really happened.
When a query is slow, you first look at the plan to see what PostgreSQL decided to do, then you look at the actual numbers to find the step that ate the time.
Example
EXPLAIN
SELECT * FROM orders WHERE customer_id = 42;
EXPLAIN ANALYZE
SELECT * FROM orders WHERE customer_id = 42;Output of the second command (numbers will differ on your machine):
Index Scan using idx_orders_customer on orders (cost=0.43..8.45 rows=1 width=64) (actual time=0.031..0.035 rows=3 loops=1)
Index Cond: (customer_id = 42)
Planning Time: 0.120 ms
Execution Time: 0.060 msHow to read one line:
| Part | Meaning |
|---|---|
Index Scan using idx_orders_customer | The operation (node) and the index it used |
cost=0.43..8.45 | Estimated startup cost and total cost, in planner units, not milliseconds |
rows=1 | Estimated number of rows this node will return |
actual time=0.031..0.035 | Real time in ms to the first row and to the last row (per loop) |
rows=3 loops=1 | Real rows returned, and how many times the node ran |
How it works
A plan is a tree. Each node feeds its rows to the node above it. Read it from the most indented line upwards: the innermost nodes run first (scans), then joins, sorts and aggregates on top.
Node names you will see often:
- Seq Scan: reads the whole table.
- Index Scan / Index Only Scan: uses an index; "only" means it did not need the table.
- Bitmap Heap Scan: collects matching locations from an index, then reads table pages in order.
- Nested Loop, Hash Join, Merge Join: the three join methods.
The most useful check is estimated rows versus actual rows. If the planner expected 10 rows and got 200,000, it probably chose a bad join method or skipped an index. That usually points to stale or missing statistics, so try ANALYZE table_name.
Also look for "Rows Removed by Filter" with a big number, which means a scan read many rows only to throw them away. An index on that filter column often helps.
Real-world use
- Checking whether a new index is actually being used before shipping it.
- Finding why a dashboard query on a large Swiggy-style orders table suddenly became slow after data grew.
- Adding
BUFFERS(EXPLAIN (ANALYZE, BUFFERS)) to see how many pages came from cache and how many from disk.
Common mistakes
- Running
EXPLAIN ANALYZEon anUPDATEorDELETEin production. It really executes the statement. Wrap it in a transaction and roll back:
BEGIN;
EXPLAIN ANALYZE DELETE FROM sessions WHERE expires_at < now();
ROLLBACK;- Treating
costas milliseconds. It is an internal unit used to compare plans. - Forgetting to multiply
actual timebyloopsfor nodes inside a nested loop. - Testing on a tiny local table. The planner picks different plans for 100 rows and for 10 million rows.
What to say in the interview
"EXPLAIN shows the planned execution tree with estimated cost and rows, without running the query. EXPLAIN ANALYZE runs it and adds actual time, actual rows and loops. I read the plan from the innermost node outwards, look for the node with the most time, and compare estimated rows with actual rows. A big mismatch usually means bad statistics, so I run ANALYZE. For writes I wrap EXPLAIN ANALYZE in BEGIN and ROLLBACK, because it really executes the statement."
Follow-up questions
- When would the planner choose a Hash Join over a Nested Loop?
- What is the difference between an Index Scan and a Bitmap Heap Scan?
- How would you find the slowest queries in a live system? (Hint: the
pg_stat_statementsextension.)
Go deeper
Read these once the basics above feel easy.
Nested Loop is good when the outer side returns few rows and the inner side has an index. Hash Join builds a hash table from the smaller input and probes it with the larger one, which suits large unsorted inputs joined on equality. Merge Join walks two inputs that are already sorted on the join key. If a sort or hash spills to disk, the plan shows lines like Sort Method: external merge Disk: ..., which hints that work_mem is too small for that query.