Skip to content
Join Algorithms — Nested-Loop, Hash, Sort-Merge
step 1/3

Reading — step 1 of 3

Join Algorithms — Nested-Loop, Hash, Sort-Merge

~2 min readJoin Algorithms

Join Algorithms — Nested-Loop, Hash, Sort-Merge

A join combines rows from two tables based on a predicate. Every database supports the same SQL syntax — but the cost difference between picking the right algorithm and the wrong one can be 10,000×.

The Three Workhorses

1. Nested-Loop Join — O(M × N)

The simplest. For every row of the outer table, scan the inner table and emit pairs where the predicate matches.

for o in outer:
    for i in inner:
        if predicate(o, i):
            emit (o, i)

Cost: |outer| * |inner| comparisons. Quadratic and brutal at scale.

Wins when: one side is tiny (a few rows), or when the inner side has an index on the join column (index-nested-loop). With an index, the cost drops to |outer| * log(inner).

2. Hash Join — O(M + N)

Build a hash table on the smaller side keyed by the join column. Probe with the other side.

build := hash_map<join_key, []row>
for r in smaller:
    build[r.key].append(r)
for r in larger:
    for match in build[r.key]:
        emit (match, r)

Cost: |smaller| + |larger| (linear). Wins when neither side is sorted and both fit in memory.

Limitation: only works for equi-joins (=). Range predicates like a.x < b.y can't hash. When the build side spills memory, hybrid hash join partitions both inputs to disk on hash buckets and joins each pair of partitions independently.

3. Sort-Merge Join — O(M log M + N log N), or O(M + N) if pre-sorted

Sort both inputs on the join column. Walk the two sorted streams with a merge cursor.

ls := sort(left,  by join_key)
rs := sort(right, by join_key)
i = 0; j = 0
while i < |ls| and j < |rs|:
    if ls[i].key < rs[j].key: i += 1
    elif ls[i].key > rs[j].key: j += 1
    else:
        # collect runs of equal keys on both sides, emit cross-product
        ...

Cost: dominated by sorting. Once sorted, the merge is linear.

Wins when: inputs are already sorted (from an index scan, or because the previous operator was a sort), or when output must be sorted anyway (ORDER BY follows).

Picking the Right Algorithm

The optimizer chooses based on cost estimates from statistics:

SituationBest join
One side ≤ ~10 rowsNested-Loop
Inner side has an index on join colIndex-NL
Equi-join, both sides large, fits memoryHash
Equi-join, both sides too big for memoryHybrid Hash
Inputs sorted on join key (index scan)Sort-Merge
Range / inequality joinNL or SMJ

You can force one in Postgres with SET enable_hashjoin = off, run EXPLAIN ANALYZE, and watch the runtime explode. That's the lesson real-world DBAs internalize.

In this lesson you'll build all three algorithms and see firsthand how their output and cost differ.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…