Why architecture matters here
Spark's cost is dominated by shuffle. Compute inside a stage is cheap; moving bytes across the network between stages is not. Skew makes it worse — one partition with 100 GB while others have 1 GB delays the entire stage on that one reducer.
The architecture matters because knobs are everywhere and their interactions are subtle. Change spark.sql.shuffle.partitions without changing executor memory and you get spill storms. Enable push-based without a matching shuffle service and gain nothing. Force broadcast when the small side is 5 GB and OOM the driver.
With the pipeline mapped, you can pick the right knob per stage and let AQE handle the rest.
The architecture: every piece explained
The top strip is the pipeline. Map tasks read input, partition their output by a Partitioner (hash by default, range or custom for special cases), sort within partition, and write partitioned files to local disk. The Shuffle service — an external process on each node — serves those files to remote reducers so map executors can be released. Reduce tasks fetch the assigned partitions from all mappers, merge-sort them, and apply the reduce function.
The middle row is the modern optimizations. Sort-based shuffle spills sorted files to disk instead of holding everything in memory. Tungsten binary encoding stores rows in off-heap unsafe format so serialization is cheap. Push-based shuffle reverses the pattern: mappers push their outputs to reducer-side shuffle services, which merge locally — dramatically reducing fetch fanout for large clusters. AQE handles skew join by splitting skewed partitions, and coalesces post-shuffle partitions if they turn out small.
The bottom rows are alternatives and observability. Broadcast alternative skips shuffle by broadcasting a small side (usually under 10 MB by default, tuned higher for larger memory). Metrics per stage show shuffle read/write bytes, records, and skew indicators. Cluster sizing — executor cores, memory, local SSD — is the physical substrate; shuffle-heavy jobs need fast local disks and headroom to spill.
End-to-end flow
End-to-end: a join between orders (500 GB) and users (2 GB) executes. The planner considers broadcast; 2 GB exceeds the threshold; a shuffle hash join is chosen. Map stage reads orders and produces partitioned files. Reduce stage fetches all partitions and joins. AQE observes stage statistics; three partitions are 20× larger than the median (skew from a small set of high-volume users). AQE splits those partitions into sub-partitions and duplicates the join key rows from the small side. The reduce stage completes 3× faster. Push-based shuffle further reduces fetch overhead by merging on the reducer's shuffle service. Metrics confirm shuffle read is 500 GB with no OOM and no failed tasks.