studyDDIAChapter 11 · Batch Processing

A Broadcast Join Moves 124x Less Data Than a Reduce-Side Join

Join a 1,000,000-row fact table to a 1,000-row dimension table two ways. A reduce-side (sort-merge) join shuffles both tables across the network — 60.5 MB. A broadcast (map-side hash) join ships only the small dim to every mapper and joins the fact table in place — 500.5 KB. Same 1,000,000 joined rows, identical checksum, but 124× less data on the wire.


Concept

Join a 1,000,000-row fact table (activity events keyed by user_id) to a 1,000-row dimension table (user profiles) with a tiny in-process MapReduce, two ways. A reduce-side (sort-merge) join emits every record of both tables keyed by user_id and shuffles them across the network to reducers — so the bytes on the wire are size(fact) + size(dim): the whole big table moves. A broadcast (map-side hash) join ships the small dim table to every mapper once, builds a local hash, and joins the fact table in place — so the fact table never crosses the shuffle and the bytes on the wire are N_mappers × size(dim). Same 1,000,000 joined rows, identical checksum — but the broadcast join moves 124× less data.

Provenance

Designing Data-Intensive Applications, 2nd ed. (Kleppmann & Riccomini, 2025), Chapter 11 — Batch Processing, §"Joins and Grouping" / §"Shuffling Data":

When a mapper emits a key-value pair, the key acts like the destination address to which the value should be delivered [...]. Bringing all the records with the same key together in the same place is the whole purpose of the sort in MapReduce.

The book contrasts reduce-side joins (sort and shuffle both inputs to a common reducer) with map-side joins (a broadcast hash join keeps a small enough input in memory on every mapper, avoiding the sort and shuffle of the large input entirely). Here you watch the byte counter separate the two by two orders of magnitude.

Prerequisites

The magnitude is set by which table crosses the network. These help; each line notes where it shows up.

  1. MapReduce = map → shuffle → reduce — a map function emits key-value pairs; the framework shuffles (partitions + sorts) so all pairs with the same key land at one reducer; the reduce function processes each key's group. It shows up as the mapper/partitioner/reducer pipeline both joins are built from.
  2. Partitioned / sort-merge (reduce-side) join — to join two tables, emit every row of both keyed by the join key; the shuffle co-locates matching rows at a reducer, which merges them. It shows up as reduce_side_join, whose shuffle carries both whole tables.
  3. Broadcast / map-side (hash) join — replicate one table to every mapper, load it into an in-memory hash table, and probe it while scanning the other table locally; no shuffle of the big table. It shows up as broadcast_join, whose shuffle carries only N_mappers copies of the dim.
  4. "Small enough to broadcast" — the broadcast side must fit in each mapper's memory. 62 KB does; 60 MB might; a second 60 MB fact table would not — that is the boundary where you fall back to a reduce-side join.
Where to learn the prerequisites MapReduce and the shuffle (#1–#2): DDIA §"MapReduce Workflows" and §"Reduce-Side Joins and Grouping"; Dean & Ghemawat, "MapReduce: Simplified Data Processing on Large Clusters" (2004) for the map/shuffle/reduce model. Map-side / broadcast hash joins (#3–#4): DDIA §"Map-Side Joins" — broadcast hash joins, partitioned hash joins, and map-side merge joins, and the memory condition that lets a table be broadcast. If only one idea is new, make it the shuffle-cost model (#2 vs #3): the byte count is whichever table you move, and that single choice carries the entire 124× result.
Environment these numbers came from Seeded, so the byte counts reproduce exactly. Captured on macOS 26.5.2 (Darwin 25.5.0), arm64, Python 3.15.0a8, standard library only — no Docker, no deps. No real cluster: mappers, the partitioner, the shuffle byte counter, and reducers are all simulated in one process, so the counts are fixed every run. Fixed workload: 1,000,000 fact events over 1,000 user ids, joined to 1,000 dim profiles; random.Random(42).

Mental model

Two joins, same two tables, same 1,000,000-row answer — only what crosses the network differs.

Both are one script, code/shuffle_vs_broadcast_join.py, with a real byte counter summing the serialized size of every record that crosses the shuffle boundary. Both must emit the identical joined output.

Setup

cd study/ddia/ch11/code
python3 shuffle_vs_broadcast_join.py

Do not read the output yet — make each prediction first.


Step 1 · the two tables

Predict. The fact table is 1,000,000 events (user_id, event, item, ts); the dim table is 1,000 profiles (user_id, name, country, plan). Roughly how many times larger is the fact table than the dim table on disk?

How much larger is the fact table?
Join a FACT table to a DIMENSION table with an in-process MapReduce. fact: 1,000,000 activity events = 60.5 MB on disk dim: 1,000 user profiles = 62.6 KB on disk the fact table is 990x larger than the dim table

60.5 MB vs 62.6 KB — the fact table dwarfs the dim table by ~990×. Hold that ratio: it is the ceiling on how much a broadcast join can save.

Step 2 · the reduce-side (sort-merge) join

Predict. Map both tables to (user_id, tagged record) and shuffle to 16 reducers, which group by user_id and join. How many bytes cross the shuffle boundary — and how does that compare to the two tables' sizes?

How many bytes cross the shuffle?
reduce-side (sort-merge) join -- shuffle BOTH tables to reducers by user_id: rows joined = 1,000,000 bytes shuffled = 60.5 MB (= size(fact) + size(dim))

60.5 MB — essentially the whole fact table. Every one of the 1,000,000 fact rows is emitted keyed by user_id and physically moved to a reducer, plus the 1,000 dim rows. The join is correct, but you paid to ship the entire big table across the network.

Step 3 · the broadcast (map-side hash) join

Predict. Now ship the 62.6 KB dim table to every one of 8 mappers, build a local hash, and join the fact table in place. How many bytes cross the shuffle now — and is the answer the same?

How many bytes cross the shuffle now?
broadcast (map-side hash) join -- ship the dim to all 8 mappers, fact stays put: rows joined = 1,000,000 bytes shuffled = 500.5 KB (= 8 mappers x size(dim))

500.5 KB — just 8 copies of the tiny dim table. The fact table never moves. Same 1,000,000 rows joined, but the network carried three orders of magnitude less than the fact table's 60.5 MB.

Step 4 · same answer, 124x less data

Predict. Both joins claim 1,000,000 rows. If we checksum the joined output (order-independent), will the two strategies match — and what is the ratio of bytes shuffled, reduce-side over broadcast?

Same checksum? What is the ratio?
same join result? reduce-side: 1,000,000 rows, checksum 175720604590266420 broadcast: 1,000,000 rows, checksum 175720604590266420 IDENTICAL -> True data moved over the network: reduce-side 60.5 MB broadcast 500.5 KB broadcast moves 124x LESS data for the SAME answer.

Identical checksum, 124× less data. A join is not a join: both produce the exact same output rows, but one moves 60.5 MB and the other 500 KB. The gap is whichever table you chose to ship.


What you should see

Why

A join needs matching rows co-located: to combine fact row 42 with profile 42, they must be on the same machine at the same time. There are only two ways to arrange that, and they differ only in which data you move.

The reduce-side join moves both sides to a common partition. The map phase tags every record with its user_id and the shuffle uses that key as a destination address — so fact rows and the profile for user 42 all get routed to the same reducer, where the merge happens. The cost is the sum of everything that crossed the shuffle: size(fact) + size(dim) ≈ 60.5 MB + 62.6 KB ≈ 60.5 MB. The dim table is a rounding error; you paid, overwhelmingly, to relocate the entire fact table across the network — and you also paid to sort it, which this counter doesn't even bill.

The broadcast join inverts the move: instead of shipping the big side to a rendezvous point, it replicates the small side to wherever the big side already sits. Each of the 8 mappers receives one copy of the 62.6 KB dim, loads it into an in-memory hash table keyed by user_id, then streams its local slice of the fact table through, probing the hash for each event. The fact table never enters the shuffle at all. The cost is N_mappers × size(dim) = 8 × 62.6 KB ≈ 500.5 KB. The ratio is therefore roughly size(fact) / (N_mappers × size(dim)) = 60.5 MB / 500.5 KB ≈ 124× — and because both joins compute the same inner join on user_id, the output rows (and their order-independent checksum) are bit-for-bit identical. The intuition shortcut: the byte bill is whichever table you move, so move the small one.

The boundary — broadcast wins only while the small side fits in memory The formula N_mappers × size(dim) beats size(fact) + size(dim) exactly when N_mappers × size(dim) < size(fact), i.e. when the dim table is small enough (relative to the fact table and the mapper count) to replicate everywhere. That "small enough" is a hard memory limit: each mapper must hold the whole broadcast side in RAM. Make both tables large — join two 60 MB fact tables — and neither fits to broadcast, the map-side trick evaporates, and you are back to a reduce-side join shuffling both. Broadcast is not a faster join; it is a bet that one side is small, and the bet pays exactly the lopsidedness of the sizes.

Go deeper

Sources: DDIA 2e, Ch. 11, §"Joins and Grouping", §"Reduce-Side Joins and Grouping", §"Map-Side Joins" · Dean & Ghemawat, "MapReduce: Simplified Data Processing on Large Clusters" (2004).