Overview
The same JOIN in SQL can compile to genuinely different execution strategies depending on table size, available indexes, and existing sort order: a nested loop scans the inner table once per outer row, cheap when the inner side is small or indexed; a hash join builds an in-memory hash table from one side and probes it with the other, efficient for large, unindexed inputs; a merge join walks two already-sorted inputs in lockstep, efficient specifically when that sort order already exists. PostgreSQL's planner picks between these automatically based on cost estimates derived from table statistics, and the configuration knobs that discourage a given method (enable_nestloop, etc.) are explicitly documented as unable to suppress a method entirely, some query shapes have no other viable plan. That's why the real fix for a bad plan is almost always better statistics or a better index, not forcing a specific join algorithm.
Quick Reference
| Join algorithm | How it works | Favored when |
|---|---|---|
| Nested loop | Scan inner table once per outer row | Inner side is small or indexed |
| Hash join | Build hash table from one side, probe with the other | Large inputs, no useful sort order |
| Merge join | Walk two sorted inputs in lockstep | Inputs already sorted (or sorting helps elsewhere) |
Syntax
EXPLAIN ANALYZE
SELECT o.id, c.name
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.created_at > now() - interval '7 days';Examples
-- Reading EXPLAIN ANALYZE output - the chosen join type is
-- right there in the plan, not something you have to guess.
Hash Join (cost=1.09..458.32 rows=1200 width=64)
Hash Cond: (o.customer_id = c.id)
-> Seq Scan on orders o
-> Hash (cost=1.05..1.05 rows=200 width=36)
-> Seq Scan on customers cPlanner method flags are for diagnosis, not production tuning
Only enable_nestloop carries the documented guarantee that it can't be suppressed entirely, some query shapes have no other viable plan. enable_hashjoin and enable_mergejoin can actually prevent their join type from being chosen. Use these settings to understand why the planner chose a plan, then fix the real cause (statistics, indexes), don't leave them disabled in production.
Visual Diagram
Common Mistakes
- Assuming a
JOINalways executes the same way regardless of table size or indexes, when the planner picks a genuinely different algorithm based on both. - Disabling a join method (
enable_hashjoin = off) in production to force a different plan, instead of treating it as a temporary diagnostic step. - Not running
EXPLAIN ANALYZEbefore assuming why a query is slow, guessing at the join algorithm instead of reading it directly from the plan. - Letting table statistics go stale (never running
ANALYZEafter large data changes) and then being confused when the planner picks a plan that made sense for the old data distribution.
Performance
- A mismatched join algorithm for the actual data shape (a nested loop over two large, unindexed tables) can be orders of magnitude slower than the plan the planner would pick with accurate statistics and the right index.
EXPLAIN ANALYZEreports both the estimated and actual row counts per plan node, a large gap between them is a direct signal that statistics are stale and the planner's cost estimates, and therefore its choices, can't be trusted untilANALYZEis re-run.
Best Practices
- Read
EXPLAIN ANALYZEoutput directly rather than guessing which join algorithm a query used. - Keep table statistics current with
ANALYZE, especially after large bulk changes, since the planner's join choice depends entirely on accurate row-count and cardinality estimates. - Add or adjust indexes to make a nested loop's inner-side lookup cheap, rather than fighting the planner's algorithm choice directly.
- Treat
enable_*planner method flags as a temporary diagnostic tool, not a production configuration to leave permanently changed.