When a query is slow, rewriting it blindly often leads to frustration. PostgreSQL already has a powerful brain — the query planner — but you need to understand what it’s thinking. The best way to do that is by reading its execution plan.
In this enhanced guide, we’ll explore:
- Why execution plan analysis is critical
- How to read and interpret EXPLAIN/EXPLAIN ANALYZE output
- How to detect inefficiencies
- Step-by-step strategies for making queries faster
- Visual examples to tie it all together
Why Execution Plans Matter
PostgreSQL’s planner decides the route your data will take:
- Which indexes to use (if any)
- The join order between tables
- Whether to sort, hash, or loop through data
- Whether to parallelize execution
If you don’t look at the execution plan, you’re guessing about performance. With it, you get a diagnostic X-ray of your query.
The Tools of the Trade
EXPLAIN
Shows the planner’s predicted execution plan:
EXPLAIN SELECT * FROM orders WHERE total_amount > 500;
EXPLAIN ANALYZE
Runs the query and shows the actual execution plan with timings and row counts:
EXPLAIN ANALYZE SELECT * FROM orders WHERE total_amount > 500;
This is where you see if reality matches the planner’s expectation.
Breaking Down the Plan
Here’s a simplified plan:
Hash Join (cost=12.50..25.30 rows=100 width=48) (actual time=0.15..0.42 rows=120 loops=1)
Hash Cond: (o.customer_id = c.id)
-> Seq Scan on orders o (cost=0.00..10.00 rows=200 width=32) (actual time=0.01..0.10 rows=220 loops=1)
Filter: (total_amount > 500)
-> Hash (cost=10.00..10.00 rows=200 width=16) (actual time=0.10..0.10 rows=200 loops=1)
-> Seq Scan on customers c (cost=0.00..10.00 rows=200 width=16) (actual time=0.01..0.08 rows=200 loops=1)
Key things to look at:
- Node Type: Hash Join, Seq Scan, Index Scan, etc.
- Cost: Estimated effort (startup..total)
- Rows: Estimated vs. actual row counts
- Actual Time: Measured execution time per node
- Loops: How often this node ran.
When estimated rows are significantly different from actual rows, you’ve identified a misestimation.
Spotting Inefficiencies
1. Sequential Scans on Large Tables
- Look for Seq Scan where you expected an index.
- Fix: Create an index or rewrite the query to be index-friendly.
2. Row Misestimation
- If rows=100 but actual shows 10,000, the planner chose a poor strategy.
- Fix: Run ANALYZE or increase statistics target.
3. Nested Loops Over Large Sets
- Nested loops scale poorly with big datasets.
- Fix: Encourage hash joins or merge joins by adding indexes or rewriting queries.
4. High Startup Cost vs. Total Cost
- High startup cost nodes delay first-row delivery.
- Fix: Push filters earlier or use partial indexes.
Strategies for Optimization
| Problem | Optimization Strategy |
| Full table scan on the filtered column | Create B-Tree or BRIN index |
| Join order seems wrong | Rewrite query with explicit JOIN order or CTE |
| Planner underestimates rows | ANALYZE or increase default_statistics_target |
| Too many loops in Nested Loop | Add index on join column, enabling Hash Join |
| Slow sort operation | Create index with same ORDER BY columns |
| High I/O cost | Use covering indexes or reduce SELECT list |
Before & After Example
Before Optimization:
Nested Loop (actual time=5000ms)
-> Seq Scan on orders (actual rows=1,000,000)
-> Index Scan on customers (actual loops=1,000,000)
After Optimization:
Hash Join (actual time=120ms)
-> Seq Scan on customers (200 rows)
-> Hash (built from filtered orders, only 2,000 rows)
- Performance gain: ~40x faster just by reading the plan, spotting the Nested Loop problem, and guiding the planner toward a Hash Join.
Key Takeaways
- Use EXPLAIN ANALYZE regularly — it’s your best debugging tool.
- Focus on actual vs. estimated rows to detect planner misestimations.
- Add or adjust indexes based on which columns are scanned or joined.
- Keep statistics fresh (ANALYZE, autovacuum tuning).
- Rewrite queries to reduce row count as early as possible.
Bonus: Visualizing Plans
Use EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) with tools like pgMustard or explain.depesz.com to generate visual, tree-like diagrams. These make complex plans much easier to digest.
By mastering execution plan analysis, you move from guesswork to data-driven query tuning — and that’s how you make PostgreSQL truly fast.