Letting the Cost Model Choose
Estimate the join's output, price each algorithm, keep the cheapest
When every table has statistics, estimate each join's rows from the distinct counts, price the nested loop, index nested loop, hash and merge joins with fixed formulas, pick the cheapest with a fixed tie-break, and print rows= and cost= on every node.
- An equality join keeps about L × R / max(ndv) rows
- Each algorithm has a cost formula in page reads and rows
- A small outer side favours the index; two large inputs favour hashing; sorted inputs favour merging
- No statistics, no choice: without them the planner keeps the nested loop
- Stale statistics can pick a worse plan, never wrong rows
Why this stage exists
You now have four ways to join, and each is the best one somewhere. An index nested loop is unbeatable for five users' orders and hopeless for all of them without an index. A hash join is the opposite. A merge join wins when both sides already come sorted. Forcing one with --join was fine for learning each; a real query needs the planner to pick.
The cost-model stage priced scans. This stage prices joins the same way: estimate how many rows each node produces, put a number on each algorithm, keep the cheapest. From here --join auto, the default, does that, as long as every table has statistics. Without them it keeps the nested loop, as the cost-model stage kept its rule plans.
How many rows does a join produce?
For users join orders on u.id = o.user_id, each order matches about one user. In general, an equality between two columns keeps about L × R / max(ndv_l, ndv_r) rows: 1000 users and 2000 orders with 1000 distinct user_id give 2000. The idea is that the column with fewer distinct values has its values all present in the other, so each row on one side meets R / ndv rows on the other. Any other condition keeps a third, as for a WHERE. A cross join keeps everything, and a LEFT JOIN at least its left rows.
An index nested loop's right child is a lookup, which estimates the rows of one value, E = floor(R / ndv) (with ndv = R for the table's key), and prints its cost for that one run. The join still uses the right table's full R, without the lookup, so the estimate is the same whatever the algorithm.
What each algorithm costs
| Join | Cost |
|---|---|
| nested loop | C(L) + rows(L) × C(R): the right side, once per left row |
| index nested loop | C(L) + rows(L) × lookup: one lookup per left row |
| hash | C(L) + C(R) + 2 × rows(R) + rows(L): each side once, building costs twice per row |
| merge | C(L) + C(R) + rows(L) + rows(R), plus rows(x) for each side that must be sorted |
On tiny tables the nested loop wins: reading a one-page table four times is nothing. On large ones the picture changes:
- five users' orders, with an index on
user_id: five lookups beat reading 2000 wide orders, so the index nested loop wins; - every user's orders, no index: the hash join reads each table once;
- users joined to a table keyed by user id: both sides already arrive in key order, and the merge join is one pass over each.
Ties go to the simpler algorithm, in the order nested loop, index nested loop, hash, merge, so every implementation prints the same plan. Ties are not rare: users joined to orders with no index costs the same by hash and by merge when users already arrives in key order and only orders needs a Sort, and the hash join is taken.
The algorithm in words
Estimating a join
How many rows come out.
- 1.Take the estimated rows of the two children, L and R, which already include the conditions pushed below the join. Under an index nested loop, R is the right table's rows under its own conditions, without the lookup: the lookup's equality is the join condition, counted once.
- 2.For each conjunct of the condition that is a column = a column, both with statistics, multiply by 1 / max(ndv of the two columns). For any other conjunct, multiply by 1/3.
- 3.rows = floor of L × R times those factors. With no condition, L × R. For a LEFT JOIN, at least L.
Choosing the algorithm
Price every algorithm that can be used.
- 1.Nested loop: C(L) + rows(L) × C(R).
- 2.Index nested loop: C(L) + rows(L) × the cost of one lookup, priced as the cost-model stage prices that scan for one value.
- 3.Hash join: C(L) + C(R) + 2 × rows(R) + rows(L), plus C(L) + C(R) again if rows(R) is above --join-memory.
- 4.Merge join: C(L) + C(R) + rows(L) + rows(R), plus rows(x) for each input x that needs a Sort.
- 5.Keep the cheapest; on a tie take the first of nested loop, index nested loop, hash, merge.
Output contract for this stage
These are the exact lines the tests check. The full definition is on the output contract page, section 73.
| Input | Output |
|---|---|
explain select * from a join b on a.k = b.k;, both analysed | NestedLoopJoin (on: a.k = b.k) (rows=6 cost=5) over SeqScan (table=a) (rows=4 cost=1) and SeqScan (table=b) (rows=6 cost=1) |
the same with --join hash | HashJoin (on: a.k = b.k) (rows=6 cost=18) |
the same with --join inl, index bk on b (k) | IndexNestedLoopJoin (on: a.k = b.k) (rows=6 cost=13) over IndexScan (table=b, index=bk, key=a.k) (rows=2 cost=3) |
explain select * from a, b; | NestedLoopJoin (rows=24 cost=5) |
What you should see when done
$droid --db choose.dbWelcome to droid!>create table a (id int primary key, k int);Create table statement executed successfully>create table b (id int primary key, k int);Create table statement executed successfully>insert into a (id, k) values (1, 1);Insert statement executed successfully (1 rows)>insert into b (id, k) values (1, 1);Insert statement executed successfully (1 rows)>analyze a;Analyze statement executed successfully>analyze b;Analyze statement executed successfully>explain select * from a join b on a.k = b.k;[PLAN] select * from a join b on a.k = b.k[PLAN] └── NestedLoopJoin (on: a.k = b.k) (rows=1 cost=2)[PLAN] ├── SeqScan (table=a) (rows=1 cost=1)[PLAN] └── SeqScan (table=b) (rows=1 cost=1)>.exitexiting.. good bye!
Common bugs in this stage
- Choosing by cost when a table has no statistics. Without them
autois the nested loop, and nothing prints an estimate. - Integer division too early. Multiply everything, then floor once.
- Pricing an algorithm that cannot be used. A hash join on
<is not a candidate. - Breaking ties differently. The order is nested loop, index nested loop, hash, merge.
- Counting the merge join's sort twice. The Sort node prints its child's cost; the join adds
rows(x).
Language notes
The page above is the same for every language. These are the only language-specific hints.
- C: keep estimates as 64-bit integers and selectivities as numerator/denominator pairs; multiply both, divide once.
- C++: a small
Fractionstruct withoperator*keeps the arithmetic exact. - Rust:
u128numerators and denominators avoid overflow for products of large row counts. - Zig:
u128and@divFloorat the end;std.math.mulcatches an overflow if you go wrong.
Checklist
One line per thing the tests check. The tags name the test cases.
- Each algorithm's cost, computed by hand on two one-leaf tables, and auto takes the cheapestjoin-cost-exact
- The join's rows: the product divided by the larger distinct count; a cross join is the product; LEFT keeps the left rowsjoin-rows-estimate
- A selective filter on users and an index on orders(user_id): the index nested loop winssmall-outer-picks-index
- Two large tables and no index: the hash join winslarge-unindexed-picks-hash
- Two inputs already in key order: the merge join wins, with no sortordered-inputs-pick-merge
- A condition with no equality leaves only the nested loopnon-equi-picks-nested-loop
- Drop the index the plan used, and the same query plans again, with the same rowsdrop-index-replans
- Without statistics, or with stale ones, the plan may differ but the rows never dostale-stats-still-correct