Think of Merge Join like zipping up two already-sorted stacks of cards: it is fast because it walks forward only, not back and forth.
Question: What is a merge join in SQL, and when would a database choose it?
Answer: A merge join is a join algorithm that compares two inputs in sorted order on the join key. It moves through both sides like two bookmarks in a book: when keys match, it outputs the joined rows; when one side is smaller, it advances that side. Databases like it when the rows are already ordered, often because of an index, or when sorting the inputs is still cheaper than other join methods.
Interview-Ready Answer: I’d say a merge join is a join strategy that works on sorted inputs. The engine scans both sides in order, compares keys, and advances whichever side is behind; when the keys match, it emits the joined rows, including duplicate combinations. It’s especially strong when the data is already ordered by an index, because then the join itself is basically a linear walk instead of repeated lookups or hashing.
A merge join is a join algorithm that assumes both inputs are sorted by the join key, or can be made sorted cheaply. The key idea is simple: if both sides are in order, you never need to restart from the beginning of either table. You just keep moving forward.
customers and orders, both sorted on customer_id.This is why merge join is often described as a linear scan after sorting. The join step itself is efficient because the pointers only move forward.
| Join Type | Best When | Main Cost | Big Idea |
|---|---|---|---|
| Merge Join | Sorted inputs | Sort or scan | Walk both sides in order |
| Nested Loops | Small outer side | Repeated lookups | For each row, probe the other side |
| Hash Join | Equality join, unsorted data | Build hash table | Hash one side, probe the other |
If the inputs are already ordered, the merge step is roughly O(n + m) comparisons, where n and m are the row counts on each side. That is the big win. If the database must sort first, total cost becomes O(n log n + m log m) for the sorts, plus the linear merge step.
Memory use is usually modest during the merge itself, but duplicate groups may need temporary buffering. The real danger is the sort: if the sort does not fit in memory, the engine can spill to disk and slow down a lot. In PostgreSQL, for example, sort memory is controlled by work_mem and the default is often around 4 MB, though that varies by version and setup. Once data spills, you may see temp files and much higher latency.
NULL = NULL is not true, so rows with null join keys do not match.LOWER(email), can block index order unless the engine has a matching expression index.Memory Hook: Picture two people walking down parallel library aisles, each reading sorted book labels. They never go backward; they only step forward until the labels match.
Imagine a checkout service in an e-commerce company that runs a nightly reconciliation job: it matches payments to orders by order_id. For months, the job finishes in under a minute because both tables have indexes on order_id, so the optimizer can use a merge join with ordered reads.
Then the business grows, the tables get much larger, and someone changes the report query so the join key is wrapped in a function. The index order is no longer usable, the database adds a sort before the merge, and the job jumps from 45 seconds to 18 minutes. Customers notice delayed refund emails, and operations sees temp file spikes and disk-heavy load. In the logs, the smoking gun is usually an execution plan with Sort nodes feeding a Merge Join, plus messages about external merge or temp disk usage.
What goes wrong: the team thinks “merge join is fast,” but the real issue is that the inputs were no longer naturally ordered. The result is not a slow join algorithm by itself; it is a slow sort plus join pipeline.
-- Merge join demo: one query shows the normal inner join behavior,
-- and the next shows an edge case with NULLs and unmatched rows.
-- The data is written so you can see duplicate matches too.
WITH customers(customer_id, customer_name) AS (
VALUES
(1, 'Ada'),
(2, 'Ben'),
(3, 'Chao'),
(NULL, 'Mystery')
),
orders(order_id, customer_id, amount_cents) AS (
VALUES
(100, 1, 2500),
(101, 1, 4000),
(102, 2, 1500),
(103, NULL, 9900)
)
SELECT
c.customer_id,
c.customer_name,
o.order_id,
o.amount_cents
FROM customers AS c
JOIN orders AS o
ON c.customer_id = o.customer_id
ORDER BY c.customer_id, o.order_id;
-- Edge case: LEFT JOIN keeps unmatched customers.
-- Notice that the NULL customer_id still does not match anything,
-- because NULL never equals NULL in a normal SQL join condition.
WITH customers(customer_id, customer_name) AS (
VALUES
(1, 'Ada'),
(2, 'Ben'),
(3, 'Chao'),
(NULL, 'Mystery')
),
orders(order_id, customer_id, amount_cents) AS (
VALUES
(100, 1, 2500),
(101, 1, 4000),
(102, 2, 1500),
(103, NULL, 9900)
)
SELECT
c.customer_id,
c.customer_name,
o.order_id,
o.amount_cents
FROM customers AS c
LEFT JOIN orders AS o
ON c.customer_id = o.customer_id
ORDER BY c.customer_id, o.order_id;Follow-up & Tricky Questions:
NULL ever match NULL in a merge join? Not with normal SQL equality semantics. If you need null-safe matching, you must use a dialect-specific operator or rewrite the logic carefully.Tricky: Is merge join automatically the fastest join because it is simple? No. Simplicity is not the same as speed; if the data must be sorted from scratch, a hash join may be faster, especially for large unsorted tables.
Tricky: If both sides are sorted, does the database still need to compare every row? Yes, it still compares keys as it walks forward. The win is that it never restarts a scan from the top.
Tricky: If a key is duplicated on both sides, does merge join collapse duplicates? No, it expands them. Matching duplicate groups produce all valid combinations for that key.
Common Mistakes:
Memory Hook: “Two sorted lines, one forward walk.” If the labels are already in order, merge join just walks them like two people reading street numbers on the same road.
Cheat Sheet:
O(n + m).O(n log n + m log m).NULL does not match NULL.Practice Tasks:
EXPLAIN in your database and see whether the optimizer picks merge join, hash join, or nested loops for the same query.