Reading Shuffle Volume

Check 2 asks one question of the slow stage: does the amount it shuffles match what its operation needs? The stage page header gives 3 totals. Input is what the stage read from storage. Shuffle Write is what its map tasks wrote for the next stage, and Shuffle Read is what its reduce tasks fetched from the stage before. The ratio to compute is a map stage's shuffle write over its input, or for a reduce stage, its shuffle read over the input of the stages feeding it.

That ratio has no single healthy value. Each operation shape has an expected value, and the verdict comes from comparing the measured ratio with it:

OperationExpected write over inputWhy
groupBy on a low-cardinality keyWell under 0.01Each map task combines its rows to one per key before writing
groupBy on a near-unique keyClose to 1Combining finds almost no repeated keys inside a map task
Shuffle join or windowAbout 0.5 to 1.5Every kept row crosses; columnar input and shuffle blocks compress differently
Broadcast join0 for the large sideThe small side ships whole and the large side stays put
After an explode or a duplicate-key joinAbove 1.5Rows multiplied before the write

Count records, then bytes

Bytes depend on encoding, so count records instead. The stage page reports Shuffle Write Size / Records, and an aggregation that combines has a hard ceiling on the records it can write: each map task writes at most one row per distinct key it saw. Stage 3 of the ranking job ran 2,800 map tasks over viewing_events, which covers 190 countries. A groupBy on country over that scan can write no more records than this, whether the table holds 9 billion rows or 90 billion:

The animation runs that combine on 15 rows of viewing minutes split into 3 partitions. Watch the shuffle: each map task sends at most 3 rows, one per country, so 15 input rows become no more than 9 shuffled ones.

views.groupBy("country").sum("minutes").collect()
views
countryminutes
US42
BR15
US30
IN22
US8
BR61
IN12
US25
BR9
IN40
US18
US33
IN5
BR27
US11

Same scan, 3 verdicts

Each shuffle write below is measured against the same 380 GB scan of viewing events. The first 2 come from the scan stage itself. The third comes from the join's reduce stage, where the joined rows enter the window's exchange.

OperationShuffle writeRatioVerdict
groupBy country, sum minutes15 MB0.00004Healthy: combining collapsed each task to 190 rows at most
groupBy profile_id, collect_list of titles361 GB0.95Expected for the key: profiles barely repeat inside a task
window by title_id after a join to device_history on device_id1.9 TB5.0Excessive: device_id repeats on both sides, so the join multiplied rows before the window's exchange

The second row deserves a closer look even though it reads as expected. Grouping by profile sends nearly every row across the network, so the question becomes whether the job needs profile-level lists at all. An expected ratio tells you the shuffle matches the code; whether the code should shuffle at that grain is a separate decision.

Where an excessive shuffle comes from

Input counts bytes read after Parquet column pruning, so a query touching 4 of 60 columns shows only those 4 columns' bytes. Measuring shuffle write against the full table size on disk makes every shuffle look healthier than it is.

A stage scans 120 GB and runs a sum of revenue grouped by store_id, with 1,000 map tasks over 500 stores. Its Shuffle Write reads 96 GB. What is the verdict?

sales.groupBy("store_id").sum("revenue")

About this lesson

This section is part of the The Spark UI: Intermediate lesson on DataDriven, the home of data engineering. Lessons cover SQL, Python, Spark, data modeling and pipeline architecture, and each section pairs its explanation with worked examples and practice you run as you go.