Skip to main content

How a Distributed Engine Splits Work, and Why the Shuffle Is Expensive

Level 6: Level 6: Cloud & Data Engineering Foundationsmedium26 mindistributed processingSpark stages and tasksnarrow vs wide transformationsthe shuffleaggregation

Data splits into partitions, a job into stages, a stage into one task per partition. Narrow steps stay put; wide steps (group-by, join) force a shuffle across the network, the dominant cost of a big job.

Why one machine is not enough

A terabyte does not fit in one machine's memory, and scanning it on one CPU takes forever. Distributed processing splits the data into partitions, ships a copy of the compute to where each partition lives, runs them in parallel, and combines the results. This is scaling out (more machines) rather than scaling up (a bigger machine), and it is how engines like Apache Spark chew through data a warehouse-load at a time.

The Spark execution model, briefly

You do not need to run Spark to interview well, but you need its vocabulary, because it is the vocabulary of the whole field.

  • A dataset is split into partitions, the unit of parallel work.
  • A job breaks into stages, and each stage runs one task per partition.
  • Tasks run on executors spread across the cluster.
Order of evaluation
  1. Partitionsthe data, split into chunks
  2. Stage 0: mapone task per partition, no movement within it
  3. Shufflerepartition so same-key rows co-locate
  4. Stage 1: reducegroup or join the co-located rows
  5. Resultcombined output
A job splits into stages; each stage runs one task per partition. A wide step needs a shuffle, the expensive move across the network.

The crucial split is narrow versus wide transformations:

Table
Narrow transformations stay within a partition. Wide transformations trigger a shuffle across the cluster.
transformationkindneeds a shuffle?
select, filter, mapnarrowno: each partition works alone
groupBy, join, distinctwideyes: rows must move to co-locate by key
repartitionwideyes: data is redistributed on purpose
Narrow transformations stay within a partition. Wide transformations trigger a shuffle across the cluster.
  • Narrow transformations (select, filter, map) touch one partition at a time and need no data to move. They are cheap and pipeline together.
  • Wide transformations (groupBy, join, distinct, repartition) need rows with the same key to end up on the same machine, which forces a shuffle.

The shuffle is the expensive thing

A shuffle repartitions the data across the network so that every row with the same key lands together. It writes intermediate files to disk on the map side, transfers them across the network, and reads them back on the reduce side, paying disk I/O, serialization, and network cost all at once. Spark's own docs call it "a complex and costly operation." When a big job is slow, the shuffle is usually why, and a common tuning knob is the number of shuffle partitions (Spark defaults to 200).

One detail that trips up beginners: the shuffle write is attributed to the producing (map) stage. So in the metrics you will query, stage 0 shows the shuffle bytes even though its own transformations are narrow, because it sits at the boundary to the wide step and writes the data the next stage reads back.

Reading the execution with SQL

An engine records per-task metrics: which stage, how many rows, how many bytes, how long. Reading those is how you understand where a job spent its time. The exercises query a task_metrics table (one row per task) to summarize the stages and see where the shuffle bytes were written.

Common mistake: assuming more partitions always means faster. Past a point, tiny partitions mean thousands of tiny tasks whose scheduling and shuffle overhead outweighs the parallelism, so partition count is a tuning choice, not a free win.

Interview nuance: "what is a shuffle and why is it slow" is a core distributed-systems question. The answer: a wide operation like a group-by or join has to move rows across the network so same-key rows co-locate, paying disk, serialization, and network cost. Narrow operations like filter and map avoid it.

On a real platform this differs. Real per-task metrics come from the Spark UI or history server, with dozens of columns. The task_metrics table here keeps the few that carry the lesson: stage, rows, bytes, shuffle write, and duration. The stage-level summary you compute is exactly the view the UI shows you.

Sample data for this example
CREATE TABLE task_metrics (
  stage_id            INTEGER,
  task_id             INTEGER,
  input_rows          INTEGER,
  input_bytes         INTEGER,
  shuffle_write_bytes INTEGER,   -- bytes this task wrote to the shuffle (0 on the read stage)
  duration_s          REAL
);
INSERT INTO task_metrics (stage_id, task_id, input_rows, input_bytes, shuffle_write_bytes, duration_s) VALUES
  (0, 0, 2500000, 260000000, 90000000, 12.0),   -- stage 0: scan + map (narrow), balanced
  (0, 1, 2500000, 258000000, 89000000, 11.5),
  (0, 2, 2500000, 262000000, 91000000, 12.3),
  (0, 3, 2500000, 259000000, 90000000, 11.8),
  (1, 0, 1200000, 120000000, 0,  8.0),           -- stage 1: reduce after the shuffle (wide)
  (1, 1, 1300000, 130000000, 0,  8.5),
  (1, 2, 1250000, 125000000, 0,  8.2),
  (1, 3, 6000000, 600000000, 0, 47.0);           -- straggler: a skewed key landed on this task
Worked example (SQL)
-- Every task, by stage. Notice stage 0 writes the shuffle; stage 1 has one slow task.
SELECT stage_id, task_id, input_rows,
       ROUND(shuffle_write_bytes / 1000000.0, 0) AS shuffle_mb,
       duration_s
FROM task_metrics
ORDER BY stage_id, task_id;

Apply

Your turn

The task this lesson builds to.

Write a query that summarizes each stage, as (stage_id, task_count, total_input_rows, avg_duration_s), in stage order, over task_metrics(stage_id, task_id, input_rows, input_bytes, shuffle_write_bytes, duration_s).

Group the tasks by stage_id, count them, sum input_rows, and average duration_s. Round avg_duration_s to 2 decimals, ordered by stage_id.

3 hints and 1 automated check are waiting in the workspace.

Practice

Make it stick

A second problem on the same idea, plus 4 bonus drills.

Write a query that returns each stage's total shuffle write in MB and its total task time in seconds, as (stage_id, shuffle_mb, total_task_seconds), in stage order, over the same task_metrics table.

Treat 1 MB as 1,000,000 bytes. Sum shuffle_write_bytes and duration_s per stage, and round both to 2 decimals, ordered by stage_id. (Notice which stage writes the shuffle and which stage still runs longer.)

3 hints and 1 automated check are waiting in the workspace.