Lazy Evaluation: Intermediate
The DAG as a Graph
- map, filter, select, withColumn
- Each output partition from ONE input partition
- Links into the chain, no graph cut
- Data flows straight through, in place
- groupBy, join, distinct, repartition
- Each output needs MANY input partitions
- Cuts the graph; starts a new stage
- Data is regrouped across the network
Reading cost off the shape of the graph
Counting Stages, Counting Shuffles
A two-stage job, made concrete
(products
.filter(F.col("in_stock") == 1)
.groupBy("category")
.agg(F.count(F.lit(1)).alias("n"))
.orderBy(F.col("n").desc(), F.col("category").asc()))Logical vs Physical Plans
| In the physical plan | What it means | Why you care |
|---|---|---|
| Exchange | A shuffle: data reorganised across the network | Each one is a stage boundary and a cost |
| PushedFilters | A filter moved down to the data source | You read less data; this is good |
| *(n) marker | Whole-stage code generation fused n operators | Those operators run as one tight loop |
| BroadcastExchange | A small side shipped to every executor | A join avoided a full shuffle |
Pipelining Narrow Operations
Stack narrow work, pay once
> From order_items, keep only rows with quantity above 1, then compute a line total as quantity times unit_price, returning item_id and that total. Both operations are narrow, so they fuse into one pass over the data.
(order_items .(F.col("quantity") > 1) .("line_total", F.col("quantity") * F.col("unit_price")) .select("item_id", "line_total"))
DAG vs Lineage
- Read forward: source to result
- Used to SCHEDULE the job into stages
- Answers: how will this execute?
- Lives for the duration of the job
- Read backward: result to source
- Used to RECOVER a lost partition
- Answers: how was this rebuilt?
- Lets Spark recompute instead of re-read
- Read the DAG and the physical plan as narrow chains cut by wide shuffles.
- Count stages to count shuffles (stages = shuffles + 1) and find the expensive one.
- Read explain() bottom up; count Exchange nodes and check for PushedFilters.
- Pack narrow work into each stage; treat every new wide op as a deliberate cost.
- Don't treat all transformations as equal cost; narrow pipelines, wide shuffles.
- Don't conflate the DAG and lineage; one schedules forward, one recovers backward.
- Don't assume a filter pushed down; confirm it in the plan, do not hope.
- Don't add a wide operation casually; each one cuts a stage and pays for a shuffle.
> You open the Spark UI for a report job that got slow and see it ran in 4 stages, with the third stage taking most of the wall-clock time. You have the code and the explain output in front of you.
The shape of the graph is the map of where your time goes.
- Category
- Spark
- Difficulty
- intermediate
- Duration
- 14 minutes
- Challenges
- 3 hands-on challenges
Topics covered: The DAG as a Graph, Counting Stages, Counting Shuffles, Logical vs Physical Plans, Pipelining Narrow Operations, DAG vs Lineage
Lesson Sections
- The DAG as a Graph (concepts: paShuffleOptimization)
When you build a chain of transformations, Spark records it as a DAG, a directed acyclic graph. Directed because the data flows one way, from source to result. Acyclic because it never loops back on itself. Each transformation you wrote is a node, and the edges show how data flows from one operation into the next. The DAG is the concrete form of the plan that laziness let Spark assemble: it is everything you described, captured as a graph, waiting for an action to execute it. The reason the DAG
- Counting Stages, Counting Shuffles (concepts: paSparkUiDiagnosis)
The DAG has a structure the Spark UI exposes directly, and it is the most useful thing to understand about reading a job: the graph is divided into stages, and the boundaries between stages fall at the wide operations. A stage is a run of work that needs no data movement, a maximal chain of narrow operations that can all pipeline together. The moment a wide operation appears and data must shuffle, the current stage ends and a new one begins on the far side of the shuffle. This gives you a rule y
- Logical vs Physical Plans (concepts: paSparkUiDiagnosis)
The DAG in the UI is the visual form of the plan; explain is the textual form, and it shows you two related things. When you call explain on a DataFrame, Spark prints the plan it intends to run, and at the detailed level it shows both the logical plan, what you asked for, and the physical plan, how Spark will actually execute it. The gap between the two is the optimisation that laziness made possible. You read a plan tree from the bottom up, because that is the order data flows: the leaves are t
- Pipelining Narrow Operations (concepts: paSparkExecutionModel)
Inside a single stage, between two shuffle boundaries, something elegant happens that explains why narrow operations cost almost nothing. Spark does not run your filter over the whole partition, write the result, then run your select over that, then write again. It fuses the narrow operations into a single pass: each row flows through the filter, then the select, then any other narrow steps, one row at a time, never landing in between. This is pipelining, and it is why a long chain of narrow ope
- DAG vs Lineage (concepts: paSparkExecutionModel)
Two words get used loosely and even interchangeably, and a careful candidate keeps them apart: the DAG and the lineage. They are built from the same dependency information, but they are read for opposite purposes and in opposite directions. Confusing them is a common interview stumble, and distinguishing them cleanly lands well. The DAG is forward-looking. Spark reads it from the source toward the result to decide how to schedule the work: where the stages fall, what runs in parallel, which task