Sort-Merge Join
Sort both inputs on the key, then walk them together
With --join merge, join on equality by sorting each input on its merge key unless it already arrives in that order, walking both in step, pairing every left row of a key with every right row of that key, and handing up rows in key order so that an ORDER BY on the key needs no sort.
- Two sorted lists can be matched in one pass, like the merge of an external sort
- Equal keys form groups, and a group on each side gives every pair
- Input that already arrives sorted costs nothing to sort
- The output is ordered, which an ORDER BY above can use
- NULLs sort next to each other but never match
Why this stage exists
The hash join reads each input once but needs memory for one of them, and its output comes in no useful order. The merge join needs both inputs sorted on the key and then almost no memory at all. Its output is sorted on the key too.
When the inputs are already sorted, which is often the case when the join key is each table's own key, the sort is free. Then a merge join is the cheapest join there is: one pass over each side.
Two cursors
Put a cursor at the start of each sorted input and compare the keys under them. The smaller one cannot match anything still to come on the other side, so move it on. When they are equal, you have found a match.
Each comparison moves at least one cursor forward, so with unique keys a merge makes at most one comparison per row of either side: 2000 for two tables of 1000 rows. A nested loop would test a million pairs. The [EXEC] line counts comparisons as merge_comparisons.
Duplicates on both sides
This is where a naive merge goes wrong. With three rows of a and three rows of b all keyed 5, the right answer is nine pairs. A merge that moves both cursors on after a match gives three.
The fix is to treat equal keys as groups: on a match, collect the whole group of right rows with that key, then pair each left row of the group with all of them. When the left side moves to its next row with the same key, the right group is still there to pair with again.
NULLs and LEFT JOIN
NULL sorts first, so NULL keys sit next to each other on both sides, and a merge that only compares "equal or not" would pair them. They must never match: skip any row whose key holds a NULL. Under a LEFT JOIN a skipped left row is padded, as is any left row the right cursor passes with no partner, and every left row left over when the right side ends.
Order for free
The output comes in key order. So order by u.id on top of this join needs no Sort, because the rows already come in exactly the order a stable sort would give them. This is the one case where a join keeps the ORDER BY of a single table's key.
The algorithm in words
Planning a MergeJoin
Make sure both inputs come sorted.
- 1.The merge keys are the conjuncts l = r between a left and a right column, as for a hash join. With none, the join is a nested loop.
- 2.An input arrives sorted when it is one entry's SeqScan or key IndexScan, or a Filter right over one, and its one merge key column is the table's key. Otherwise put a Sort on its merge key columns, ascending, above it.
- 3.If ORDER BY is one ascending key that is the left column of the first merge key, or the right one for an inner join, drop its Sort.
Running a MergeJoin
Two cursors, moving forward only.
- 1.Skip rows whose key holds a NULL; under LEFT JOIN hand a skipped left row up padded.
- 2.Compare the current left key with the current right key, and count the comparison. If the left one is smaller, move the left side on, padding the row under LEFT JOIN if it never paired. If the right one is smaller, move the right side on.
- 3.If they are equal, collect the right rows with that key. For each left row with that key, in order, pair it with each of them, test the residual, and hand up the pairs that pass.
- 4.An inner join stops as soon as either side ends. A LEFT JOIN stops when the left side ends; if the right side ends first, it pads every remaining left row.
Output contract for this stage
These are the exact lines the tests check. The full definition is on the output contract page, section 72.
| Input | Output |
|---|---|
--join merge, users join orders, order by u.id | MergeJoin (on: u.id = o.user_id) over SeqScan (table=users, alias=u) and Sort (key=o.user_id ASC), and no Sort above it |
| a join on both tables' keys | [EXEC] rows_examined=<n> merge_comparisons=<c>, no sort_runs |
users join orders, --sort-memory 10 | ... merge_comparisons=<c> sort_runs=20 |
What you should see when done
$droid --db merge.db --join mergeWelcome 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, 5);Insert statement executed successfully (1 rows)>insert into a (id, k) values (2, 5);Insert statement executed successfully (1 rows)>insert into b (id, k) values (1, 5);Insert statement executed successfully (1 rows)>insert into b (id, k) values (2, 5);Insert statement executed successfully (1 rows)>select count(*) from a join b on a.k = b.k;COUNT(*)4(1 rows)[EXEC] rows_examined=4 merge_comparisons=4 sort_runs=2>.exitexiting.. good bye!
The number after merge_comparisons depends on how your merge walks its groups; this one compares each left row with each row of the right group.
Common bugs in this stage
- One pair per key. Both cursors must not move on together after a match.
- Matching neighbouring NULLs.
- Sorting an input that is already in key order. The rows are right, but there is a
Sortin the plan andsort_runson the line. - Forgetting the left rows after the right side ends under a LEFT JOIN.
- Reading on after one side ends in an inner join. Nothing more can match, and
rows_examinedshows it. - Keeping the Sort for ORDER BY the key. The merge already gives that order.
Language notes
The page above is the same for every language. These are the only language-specific hints.
- C: hold the current right group in a growable array of rows; refill it when the key changes.
- C++: a
std::vector<Row>for the right group, cleared and refilled per key. - Rust: a
Vec<Row>for the group andOrderingfrom comparing key tuples drives the loop with amatch. - Zig: an
ArrayList(Row)for the group, reset withclearRetainingCapacity.
Checklist
One line per thing the tests check. The tags name the test cases.
- Three rows and three rows on one key give nine pairsduplicates-both-sides
- Two inputs that arrive in key order are not sortedsort-skipped-when-ordered
- An input not in key order is sorted first, with --sort-memory as for ORDER BYsort-when-needed
- Without ORDER BY a merge join gives key order: left order, then right order within a keymerge-order
- Neighbouring NULL keys on both sides are never matchednull-keys
- A LEFT JOIN by merge pads users with no ordersleft-join-supported
- With unique keys, a merge compares at most once per row of either sidecomparisons-linear
- explain: MergeJoin over its inputs, a Sort above the one not in key order, none above the join for ORDER BY its keymerge-plans