Joins: Nested-Loop & Hash Join
Building a Database Engine: Transactions & Concurrency
Chapter 8 · Joins: Nested-Loop & Hash Join
Chapter 7's own capstone joined customers to orders by hand — build a Python dict once, look each order up in it. That was already a hash join, just not named as one. This chapter builds two real join algorithms properly, verifies they agree with each other exactly, measures a real speed difference at scale, and finds a genuine correctness bug hiding in the "obvious" way to build a hash table.
Two Real Join Algorithms
Finding 1: The Two Algorithms Agree Exactly
nested_loop_join and hash_join: both find exactly 4 pairs, and as sets they're identical. Two structurally very different algorithms produce the same answer, as they must.
Finding 2: A Real, Measured 44× Speedup
≈640,000 comparisons. Hash join builds one dict in 800 steps, then does 800 O(1) lookups — ≈1,600 steps total.
Finding 3: A Real, Silent Correctness Bug — Building on the Wrong Side
customers.id is unique — one row per id. orders.customer_id is not — Alice alone has two orders sharing the same key. The most natural first version of a hash join's own build step just assigns a key to a row, without thinking about what happens if that key shows up twice:
orders (the duplicate-key side), keyed by customer_id: the naive version finds only 3 pairs, not 4. Order 100 ($50, one of Alice's own two real orders) is completely missing — silently overwritten in the dict by order 101, the last one inserted for customer_id=1. No error, no warning — just one fewer row in the final result than the true join actually has. Extending this with a customer who has three orders (Exercise 3) confirms the pattern generalizes: every duplicate past the first is silently dropped, not just one.
The fix is the same one shown in this chapter's own hash_join from the start: hash_table.setdefault(key, []).append(row) instead of a bare assignment. Every key maps to a list of rows, so a collision means "append," never "overwrite."
Finding 4: Once Fixed, Either Side Is Safe to Build On
customers (the unique-key side) and on orders (the duplicate-key side) both produce the exact same 4-pair result, confirmed by set comparison. The real-world advice to build a hash join on the smaller table is about performance and memory — not correctness. Correctness only depends on never assuming a key is unique on the build side unless it genuinely, provably is.
Where This Connects
| This chapter's finding | What it connects to |
|---|---|
| Nested-loop's O(n×m) vs. hash join's O(n+m) | Maths for Programmers: Algorithms & Complexity — the exact same growth-rate reasoning, this time measured on a real join instead of an abstract example |
| A real customer, real orders, joined by real code | Chapter 7 (this course) — the manual dictionary-based join in that chapter's own capstone was already an unnamed hash join; this chapter names it, generalizes it, and finds the bug hiding in the shortcut version |
| "Confidently wrong is worse than a crash" | This site's own recurring theme — the naive hash join doesn't error, doesn't warn, just quietly returns fewer rows than the true answer |
Hands-On Exercises
Join a table of one customer against a table of one order whose customer_id doesn't match that customer's own id at all. Confirm both nested_loop_join and hash_join correctly return an empty result, with no error and no special-case handling needed in either.
Instrument both join functions to count real operations — every comparison for nested-loop, every build-plus-probe step for hash join — and run them against this chapter's own 3-customer, 4-order data. Confirm the counts match rows_left × rows_right and rows_left + rows_right exactly, not just approximately.
Give one customer three orders instead of two, and run the naive overwrite hash join built on the orders side. Confirm it now finds only 1 pair instead of the true 3, and explain why the number of silently dropped rows scales with the number of duplicates for that key, not capped at just one.
📄 View solutionChapter 8 Quick Reference
- Nested-loop join: a full inner scan for every outer row — O(n×m), correct by construction, no assumptions about key uniqueness needed
- Hash join: build a dict from one side, probe from the other — O(n+m), but only correct if the dict maps each key to a list of rows
- Verified Finding 1: both algorithms agree exactly on real customer/order data
- Verified Finding 2: a real, measured ≈44× speedup at 800×800 rows, with identical results
- Verified Finding 3: a naive overwrite-based hash join silently drops every duplicate-key row but the last one seen — no error, just fewer results
- Verified Finding 4: once the hash table is list-based, either side is safe to build on — the "build on the smaller table" rule is about performance, not correctness
- Next chapter: A Simple Query Planner — choosing an index and a join strategy automatically, verified against a naive always-scan baseline