Problem 95 · Data Engineering & Streaming
Hash Join and Sort-Merge Join
01Problem
Implement an inner equi-join of two lists of dict rows on given key columns, once as a hash join and once as a sort-merge join, handling duplicate keys and NULLs.
02Key insight
Hash join builds a hash table on the smaller side and probes with the larger: O(n+m) but needs the build side in memory. Sort-merge join is O(n log n + m log m), streams well when inputs are already sorted, and must emit the cross product of equal-key groups. SQL NULL keys never match.
03Solution
Try it first.
TimeHash: O(n+m); SMJ: O(n log n + m log m)
SpaceHash: O(min(n,m)); SMJ: O(n+m) for sorting
tests.py · 4 passing
tests.pypython13 lines
orders = [{"oid": 1, "cid": 10}, {"oid": 2, "cid": 20}, {"oid": 3, "cid": 10},
{"oid": 4, "cid": None}, {"oid": 5, "cid": 99}]
customers = [{"id": 10, "name": "ann"}, {"id": 10, "name": "ann2"}, {"id": 20, "name": "bo"},
{"id": None, "name": "ghost"}]
canon = lambda rows: sorted(tuple(sorted(r.items())) for r in rows)
hj = hash_join(orders, customers, "cid", "id")
smj = sort_merge_join(orders, customers, "cid", "id")
assert canon(hj) == canon(smj)
assert len(hj) == 5 # 2 orders x 2 dup customers for cid=10, plus cid=20
assert all(r["cid"] == r["id"] for r in hj) and not any(r["name"] == "ghost" for r in hj)
nan = float("nan") # same object both sides: still must not match, and must not hang
L2, R2 = [{"k": nan}, {"k": 1}, {"k": None}], [{"k": nan}, {"k": 1}, {"k": None}]
assert sort_merge_join(L2, R2, "k", "k") == [{"k": 1}] == hash_join(L2, R2, "k", "k")04Staff-level follow-up
The build side doesn't fit in memory or is skewed - what now? Grace hash join: partition both sides by hash(key) to disk and join partition pairs; for skewed hot keys, split them across workers and broadcast the matching rows from the other side.