PySpark Shuffle: The Complete Mental Model
Every wide transformation triggers a shuffle. Most engineers know this. Few have a clear mental model of what actually happens — the partition lifecycle, the disk writes, the network transfers, the sort. Without that model, you're guessing when you tune. With it, you can look at a Spark UI and immediately know what's wrong.
A shuffle happens in three phases: map, shuffle write, shuffle read.
The map phase: Each executor runs its tasks on its input partitions. At the end of a map task, the output is partitioned by the shuffle key (hash of the join/group-by key). Each map task writes one output file per downstream reducer.
Shuffle write: The map output is spilled to local disk. If executor memory is exceeded before the map phase completes, Spark spills partial output to disk and merges later. This is the first place skew kills you: a task with a hot key writes 10× the data of others.
Shuffle read: Each reduce task reads its designated partition from every mapper. If you have 200 mappers and 200 reducers, that's 40,000 small file reads. The network transfer is proportional to data volume, not task count — but the connection overhead scales with task count.
The key metrics in Spark UI:
The key dials:
AQE is not magic. It can't save a job with a 5000:1 key skew if you haven't enabled AQE skew join (`spark.sql.adaptive.skewJoin.enabled=true`). And even with skew join enabled, extremely hot keys (> 20× median) may require explicit salting.