Block Nested Loop Join: The Simple Double Loop
Concept. Block nested loop join reads the inner table once into a RAM-sized block, then scans the outer table once for each block, matching rows with the predicate as it goes.
Intuition. When you need to join a small Songs table to a big Listens table, load Songs into RAM in one block, then scan Listens once and check each listen against every song in the block. Cost: read Songs once, read Listens ⌈P(Songs)/B⌉ times.
The Algorithm: Load Blocks, Scan Everything
A double-for-loop in disguise. Outer loop loads blocks of B pages from the smaller table; inner loop scans the entire larger table for each block; the join predicate fires on every pair.
Example: Songs ⋈ Listens
Find all listens for each song
Given:
- P(Songs) = 100 pages
- P(Listens) = 10,000 pages
- Buffer B = 10 pages
BNLJ Cost:
- Read Songs: 100
- Blocks: ⌈100/10⌉ = 10
- Scan Listens: 10 × 10,000 = 100,000
- Total: 100,100 IOs
Wrong Choice: Listens as Outer
If we used BNLJ(Listens, Songs):
- Blocks: ⌈10,000/10⌉ = 1,000
- Scan Songs: 1,000 × 100 = 100,000
- Total: 110,000 IOs (10% worse!)
Non-Equality Joins: Where BNLJ Shines
BNLJ handles ANY join condition!
Unlike other JOIN algorithms, BNLJ supports:
- Similarity:
similarity(s.features, a.style) > 0.8 - Complex conditions:
s.duration > a.avg_length * 1.5 AND s.genre = a.genre
Real Example: Find Songs Similar to Artist's Style
-- Find songs similar to what the artist usually makes
SELECT s.song_id, a.artist_id
FROM Songs s
JOIN Artists a ON distance(s.audio_vector, a.style_vector) < 0.2
AND s.duration BETWEEN a.min_duration AND a.max_duration;
-- Only BNLJ can handle this complex join!
When BNLJ Wins
Use BNLJ When:
- Non-equality joins
- One table is small
- Large buffer available
- No indexes exist
Avoid BNLJ When:
- Both tables are huge
- Simple equality join
- Tables pre-sorted
- Small buffer
Next
Hash Partition Join → When both tables are too big for BNLJ and the join is an equality, hash partitioning turns a quadratic problem into a linear one.