droid
Sort-Merge Join
Joins and Relations · Stage 7 of 13

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.

Not run yet · 8 tests
Concepts in this stage
  • 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
Where this stage sits in the engine
Talking to the user
REPL
Sessions
Printer
Server
Wire protocol
Understanding SQL
LexerParserValidator
Answering a query
Planner
Executor
Keeping changes safe
TransactionsWrite-ahead log
Undo log
Lock manager
Read views
Storing rows
Row formatSlotted pageB+Tree
Reaching the disk
PagerDatabase file
this stagebuilt alreadystill to comeA statement enters at the top; rows come back up.

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.

--join merge: users u join orders o on u.id = o.user_id
MergeJoin (on: u.id = o.user_id)
↑ already in id order
SeqScan (table=users, alias=u)
ran
Sort (key=o.user_id ASC)
SeqScan (table=orders, alias=o)
users arrive in key order, so only orders need a Sort. The output comes out in u.id order.

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

1

Planning a MergeJoin

Make sure both inputs come sorted.

  1. 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. 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. 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.
2

Running a MergeJoin

Two cursors, moving forward only.

  1. 1.Skip rows whose key holds a NULL; under LEFT JOIN hand a skipped left row up padded.
  2. 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. 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. 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.

InputOutput
--join merge, users join orders, order by u.idMergeJoin (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

shell
$droid --db merge.db --join merge
Welcome 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
>.exit
exiting.. 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 Sort in the plan and sort_runs on 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_examined shows 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 and Ordering from comparing key tuples drives the loop with a match.
  • Zig: an ArrayList(Row) for the group, reset with clearRetainingCapacity.

Checklist

One line per thing the tests check. The tags name the test cases.

  • Three rows and three rows on one key give nine pairs
    duplicates-both-sides
  • Two inputs that arrive in key order are not sorted
    sort-skipped-when-ordered
  • An input not in key order is sorted first, with --sort-memory as for ORDER BY
    sort-when-needed
  • Without ORDER BY a merge join gives key order: left order, then right order within a key
    merge-order
  • Neighbouring NULL keys on both sides are never matched
    null-keys
  • A LEFT JOIN by merge pads users with no orders
    left-join-supported
  • With unique keys, a merge compares at most once per row of either side
    comparisons-linear
  • explain: MergeJoin over its inputs, a Sort above the one not in key order, none above the join for ORDER BY its key
    merge-plans