Narrow vs wide transformations, lazy evaluation, and how an action triggers a DAG that becomes stages and tasks.
⏱ 10 min readTopics chapter readerLevel · Foundations
01 · Orientation
Start here
⏱ 5 min · Topic 1 of 10
In the chapter "What Apache Spark Is & Why It Exists", Spark did not filter or add up any rows until show(). This chapter explains why. It is one of the most asked Spark ideas in interviews.
You will learn four things. Each one has one or two sections.
Every Spark step is a transformation or an action.
Spark waits until an action, and this wait helps Spark.
Narrow and wide steps decide how Spark splits the work.
What happens when you run two actions, or when a step fails.
We use the shop again. The job is the same: total paid revenue for each customer.
The chapter "Spark Architecture: Driver, Executors & Cluster Managers" used a model with 3 partitions of 2 orders each. We use the same model here.
02 · Two kinds of steps
Transformations and actions
⏱ 7 min · Topic 2 of 10
A transformation describes a new DataFrame from an old one. It gives you a DataFrame back. It does not run.
An action asks for a result. It prints rows, counts rows, or writes a file. An action makes Spark run the work.
Here is the whole shop job as 5 steps. Look at the code first.
Orders 103 (refunded) and 105 (pending) are filtered out. Customer 1 gets 40.00 + 25.00. Row order can differ from run to run.
Now put each step into one of the two kinds. The last column uses a word we explain in the next sections.
Step
Kind
Narrow or wide?
1 read.parquet(...)
Transformation
Not applicable
2 filter(status = paid)
Transformation
Narrow
3 withColumn(order_month)
Transformation
Narrow
4 groupBy(customer_id).sum(amount)
Transformation
Wide
5 show()
Action
Not applicable
A quick test: does the call return a DataFrame? Then it is a transformation.
Does it return a value or write output? Then it is an action.
Common actions are show(), count(), collect(), take(), and write. Common transformations are select, filter, withColumn, join, groupBy(...).agg(...), and orderBy.
03 · The key idea
Nothing runs until show()
⏱ 6 min · Topic 3 of 10
Spark is lazy. A lazy tool waits and does nothing until it must. When you write steps 1 to 4, Spark only writes them down as a plan.
When you call an action, Spark looks at the whole plan. Then it runs it. Look at what Spark does after each line of the shop job.
Line
Order rows read so far
Job started?
1 read.parquet(...)
0
No
2 filter(...)
0
No
3 withColumn(...)
0
No
4 groupBy(...).sum(...)
0
No
5 show()
6
Yes
Line 1 reads only the small footer of the Parquet file. The footer holds the column names and types. It holds no order rows.
Lines 2 to 4 only add to the plan. Line 5 is the action. Only then does Spark read the 6 orders.
Watch out
Some lines read data early. spark.read.csv(..., inferSchema=True) scans the file at once to guess the column types. That is why this chapter uses Parquet.
This is also why you can write ten transformations and see nothing happen. You did not make a mistake. Spark is waiting for an action.
Interview question
The question is "Why does Spark wait until an action?" A good answer: "Spark sees the whole plan first. It can make the plan cheaper before it runs. For example, it can skip columns it does not need."
04 · Why waiting helps
Read the plan with explain()
⏱ 7 min · Topic 4 of 10
The call explain() prints the plan Spark made. It runs no job and shows no rows. Here is the plan for the shop job.
What revenue.explain() prints (simplified)worked example
All partial sums for one customer must meet in one place.
Rows move over the network.
Left: each task keeps its own paid orders and nothing moves. Right: the partial sums for one customer must be brought together.
Narrow steps next to each other run together inside one task. Each task passes its rows through filter and withColumn in one pass. It does not save results between the two steps.
Here are common steps of each kind.
Narrow (no shuffle)
Wide (shuffle)
filter, withColumn
groupBy with an aggregate
union
distinct, orderBy, repartition
select (any narrow step)
join (in most cases)
A shuffle is slow for two reasons. It sends rows over the network, and it writes them to disk first. The shuffle chapter teaches it in depth.
06 · From code to work
From code to stages
⏱ 8 min · Topic 6 of 10
Your steps form a plan. Spark draws the plan as a graph of steps. The name for this graph is a DAG, short for directed acyclic graph.
The steps go one way and never loop back.
When the action runs, Spark cuts the DAG at each wide step. Each piece is a stage. A stage runs one task for each partition.
Our shop job has one wide step, the groupBy. So it has two stages. Look at the three tasks of stage 0.
Stage 0: three tasks, one per partitionworked example
Task 1 drops refunded order 103. Task 2 drops pending order 105. No row crossed a partition yet.
Next comes the shuffle. Spark sends each customer's partial sums to one place.
Here every customer sits in only one partition. Spark does not know that, so it still shuffles.
Stage 1 then adds the partial sums for each customer. This gives the final rows 1: 65.0, 3: 15.5, 4: 80.0. The picture shows the cut.
Stage 0
read → filter → withColumn → partial sum
3 tasks, one for each partition (0, 1, 2)
→↓shuffle (Exchange)
Stage 1
add partial sums → show()
Final totals for customers 1, 3 and 4
One wide step gives two stages. The action show() starts the job. The rule for a simple chain: stages = wide steps + 1. Real jobs can show more stages in the Spark UI.
Add orderBy("customer_id") after the groupBy and you add a second wide step. In a job that writes the result, this means one more shuffle, so one more stage. A later chapter shows how many tasks stage 1 has.
Interview question
The question is "What creates a stage boundary?" A good answer: "A wide transformation. It needs a shuffle, so Spark ends one stage there and starts the next."
07 · A real cost
Two actions, two jobs
⏱ 5 min · Topic 7 of 10
Each action starts at least one new job. Spark does not remember the first result. Look at two actions on the same paid orders.
count() then show() on the same DataFrameworked example
Job 1 reads all 6 orders and counts 4 paid ones. Job 2 reads all 6 orders again and prints the same 4.
The file was read twice. For 6 rows this costs nothing. For a big file on cloud storage, you pay the read two times.
You can keep the result in memory with cache(). Then the second action does not read the file again. The caching chapter in week 11 teaches how to use it.
Watch out
An action inside a loop starts one job for each pass. Ten passes means ten jobs, and ten reads of the source.
08 · A real failure
When errors appear
⏱ 8 min · Topic 8 of 10
Laziness changes when you see an error. It depends on the kind of mistake.
First, a wrong column name. Spark checks every name against the table's columns as soon as you run the line. This check is called analysis.
Cannot resolve column statuss. Did you mean status?
No action was called. Spark stopped on the line with the typo. The exact message changes with the Spark version. With Spark Connect (for example, Databricks serverless), this check can wait until an action. The error is the same.
Second, a bad value. Spark cannot see the values in your rows until the tasks run.
A UDF (user-defined function) is a Python function that you give to Spark. Spark runs it inside the tasks, row by row.
Here is a UDF we wrote on purpose to fail. It divides 100 by the amount minus 40. DoubleType says the result is a decimal number.
A bad value fails only at the actionworked example
Job aborted: Task 0 in stage 7.0 failed 4 times … ZeroDivisionError: float division by zero
Order 101 has amount 40.00, so 40.00 - 40.0 is 0. Task 0 holds order 101. The line with withColumn ran without an error.
Spark retried Task 0, as the architecture chapter showed. A bad value fails again on every try, so the job stops.
On a cluster, Spark tries a task 4 times. In local mode it tries only once, so you see "failed 1 times".
Watch out
The error points at show(), not at the withColumn line that caused it. To find the cause, read the message and look for the failing task and the Python error.
09 · Recap
Check yourself
⏱ 4 min · Topic 9 of 10
Four points to keep.
Transformations build a plan. Actions run it. Each action starts its own job (or more).
Spark is lazy so that it can see the whole plan. It can then prune columns and push filters down.
A wide transformation needs a shuffle, and a shuffle is a stage boundary.
Errors in names show at once. Errors in values show at the action.
Answer these without looking back.
Is groupBy(...).sum(...) a transformation or an action? Is it narrow or wide?
In the shop job, which line starts the job?
You call count() and then show() on one DataFrame. How many jobs run? How can you avoid the second read?
Why does a typo in a column name fail early, but a division by zero fails late?
10 · Next Chapter
Next Chapter
You now know when Spark runs work, and where a stage starts.
⏱ 3 min · Topic 10 of 10
Next chapter
DataFrames & Datasets: The Structured APIs
You now know when Spark runs work, and where a stage starts.
Next you learn DataFrames in more depth: schemas, columns, and the steps you will write every day.