Lazy Evaluation in Apache Spark: Why Your Code Doesn’t Run When You Write It

Understanding transformations, actions, execution plans, and why Spark waits before processing your data.

If you have worked with PySpark, you may have noticed something unusual.

You can write several transformations:

df = spark.read.parquet("sales/")
df = df.filter(df.amount > 1000)
df = df.select("customer_id", "amount")
df = df.groupBy("customer_id").sum("amount")

And Spark may appear to do nothing.

No computation.
No immediate scan of the entire dataset.
No obvious execution.

But the moment you run:

df.show()

or:

df.count()

Spark suddenly starts working.

Why?

The answer is lazy evaluation.

Lazy evaluation is one of the most important concepts for understanding Spark performance. It explains why Spark can optimize a sequence of operations before actually processing the data.

In this tutorial, we’ll understand:

  • What lazy evaluation means
  • Transformations vs actions
  • How Spark builds an execution plan
  • Why filter() doesn’t immediately execute
  • What happens when an action is called
  • How lazy evaluation enables optimization
  • Common misconceptions
  • How caching changes the execution behavior

APACHE SPARK LEARNING PATH

✓ 1. What is Apache Spark?
✓ 2. Why Spark is Faster than Hadoop
✓ 3. Spark Architecture
✓ 4. Driver vs Executor
✓ 5. Cluster Managers
✓ 6. RDD vs DataFrame vs Dataset
7. Lazy Evaluation
○ 8. DAG
○ 9. Stages and Tasks

View All Apache Spark Tutorials →


1. What Is Lazy Evaluation?

Lazy evaluation means that Spark does not immediately execute transformations when you write them.

Instead, Spark records the operations you want to perform and builds an execution plan.

The actual computation starts only when you call an action.

Consider:

df = spark.read.parquet("sales/")
filtered_df = df.filter(df.amount > 1000)
selected_df = filtered_df.select(
"customer_id",
"amount"
)

At this point, Spark hasn’t necessarily processed all the data.

It has essentially recorded:

Read sales
Filter amount > 1000
Select customer_id, amount

Now suppose we execute:

selected_df.show()

show() is an action.

This tells Spark:

“I actually need the result now.”

Spark can then create and execute the necessary physical plan.


2. Transformations vs Actions

Lazy evaluation becomes much easier to understand when you know the difference between transformations and actions.

Transformations

Transformations describe what should happen to the data.

Examples include:

filter()
select()
withColumn()
groupBy()
join()
orderBy()
drop()

For example:

filtered_df = df.filter(df.salary > 50000)

This doesn’t mean:

“Immediately scan the entire dataset and filter it.”

Instead, it means:

“When you eventually need the result, apply this filter.”


Actions

Actions actually trigger computation.

Common examples include:

show()
count()
collect()
first()
take()
write()

For example:

df.filter(df.salary > 50000).count()

The count() triggers execution.

Conceptually:

Transformation
Transformation
Transformation
Action
Execution

This distinction is fundamental to Spark.


3. A Simple Example

Consider:

df = spark.read.parquet("employees/")
df1 = df.filter(df.salary > 50000)
df2 = df1.select(
"employee_id",
"department",
"salary"
)

Have we processed the data?

Not necessarily.

Spark has a logical representation of what you want.

Now:

df2.show()

The action triggers execution.

The overall flow becomes:

Read Data
Filter
Select
Show

This is why Spark is often described as lazy.


4. Why Doesn’t Spark Execute Immediately?

At first, lazy evaluation might seem inefficient.

Why not simply execute every line as soon as it is written?

Because Spark can make better decisions when it sees the complete computation.

Consider:

df = spark.read.parquet("sales/")
df = df.filter(df.country == "US")
df = df.select(
"customer_id",
"amount"
)
df = df.filter(df.amount > 1000)
df.count()

If Spark executed every statement immediately, it would have fewer opportunities to optimize the complete computation.

Instead, Spark can look at the entire operation chain before execution.

Conceptually:

Read
Filter country = US
Select columns
Filter amount > 1000
Count

Spark’s optimizer can then determine an efficient way to execute this plan.

This is one of the major advantages of lazy evaluation.


5. Lazy Evaluation and Query Optimization

Lazy evaluation is closely connected to Spark’s optimization framework.

For DataFrame and Spark SQL workloads, Spark can analyze the logical plan and optimize it before generating the physical execution plan.

For example:

df.filter(df.age > 30).select("name")

Spark knows that the final result only needs:

name

and rows where:

age > 30

The optimizer can therefore look for opportunities to reduce unnecessary work.

Two important ideas you’ll encounter are:

  • Predicate pushdown
  • Column pruning

Predicate Pushdown

Suppose your data is stored in Parquet.

You write:

df.filter(df.country == "US")

Instead of blindly reading every record and filtering afterward, Spark and the underlying data source may be able to push the filter closer to the data source.

Conceptually:

Without optimization:
Read everything
Filter US records

Potentially:

With predicate pushdown:
Read relevant data
Filter US records

Less data may need to be read.


Column Pruning

Suppose your table contains:

customer_id
name
email
country
age
salary
address
phone
...

But your query only needs:

df.select("customer_id", "salary")

Spark can potentially avoid reading unnecessary columns from columnar storage formats such as Parquet.

This reduces I/O and data processing.


6. Lazy Evaluation Doesn’t Mean Nothing Happens

One common misconception is:

“Spark does absolutely nothing until an action.”

That’s an oversimplification.

Spark can construct and analyze execution plans before the actual distributed computation is triggered.

For DataFrames and Spark SQL, Spark has multiple stages of planning and optimization.

A simplified view is:

Your PySpark Code
Logical Plan
Optimized Logical Plan
Physical Plan
Execution

The important point is that the actual distributed computation is triggered by an action.


7. What Happens When You Call an Action?

Consider:

result = (
df
.filter(df.amount > 1000)
.groupBy("customer_id")
.sum("amount")
)
result.show()

Before show(), Spark has a chain of operations.

When show() is called, Spark can construct the required execution plan and submit the work for execution.

A simplified flow is:

PySpark Code
Transformations
Logical Plan
Optimization
Physical Plan
DAG
Stages
Tasks
Executors
Result

The details of DAGs, stages, and tasks are important enough to deserve their own tutorials.

We’ll cover those next.


8. Multiple Transformations Don’t Mean Multiple Immediate Jobs

Consider:

df1 = df.filter(df.age > 30)
df2 = df1.select("name", "age")
df3 = df2.withColumn(
"age_group",
df2.age + 10
)

It is tempting to think:

filter → execute
select → execute
withColumn → execute

That’s not how Spark’s lazy execution model works.

These operations can remain part of the same computation plan until an action requires the result.

For example:

df3.show()

can trigger the computation.


9. Narrow Transformations and Lazy Evaluation

Lazy evaluation becomes especially powerful when combined with Spark’s transformation model.

Consider:

df.filter(df.age > 30).select("name")

Both operations can often be performed without requiring a shuffle.

Spark can pipeline such operations within the same stage.

Conceptually:

Input Partition
Filter
Select
Output

Instead of unnecessarily materializing the result after every operation.

This concept becomes important when understanding narrow and wide transformations.

You might need this next

Not all transformations behave the same way.

Learn how Spark determines whether an operation requires a shuffle:

→ Narrow Transformations
→ Wide Transformations
→ Shuffle

Deep Dive into Spark Transformations →


10. What About show()?

This is a common interview question.

Suppose you write:

df.filter(df.salary > 50000).show()

Does filter() execute?

Not by itself.

The show() is an action that causes Spark to execute the necessary computation to produce the requested output.

This is why using:

df.show()

repeatedly during development can become expensive when working with large datasets.

For example:

df.filter(condition1).show()
df.filter(condition2).show()
df.filter(condition3).show()

Each action can trigger computation.

If the underlying DataFrame is expensive to compute, repeatedly triggering actions can result in repeated work.


11. What Happens If We Call count()?

Consider:

df = spark.read.parquet("transactions/")
filtered = df.filter(df.amount > 1000)

Nothing significant is necessarily computed yet.

Now:

filtered.count()

count() is an action.

Spark needs to actually process the relevant data to determine the count.

Then:

filtered.show()

is another action.

Without persistence, Spark may need to execute the required computation again.

This leads to another important Spark concept:

Caching and persistence.


12. Lazy Evaluation and Caching

Suppose you have an expensive transformation:

processed = (
df
.join(customer_df, "customer_id")
.filter(df.amount > 1000)
)

And you use it multiple times:

processed.count()
processed.show()
processed.write.parquet("output/")

Without caching, Spark may recompute the lineage required for each action.

If the DataFrame is expensive to generate and reused multiple times, you can consider:

processed.cache()

Then:

processed.count()
processed.show()
processed.write.parquet("output/")

The first action materializes the cached data, and subsequent actions can reuse it when the cache remains available.

However, caching everything is not a performance optimization.

Caching consumes cluster memory and can actually make a workload slower if used unnecessarily.

Going deeper

Wondering what actually happens after you call cache()?

Learn:

→ Cache vs Persist
→ Storage Levels
→ When caching helps
→ When caching hurts

Caching and Persistence in Spark →


13. Lazy Evaluation vs Eager Evaluation

The difference can be summarized simply.

Eager evaluation

An operation executes immediately.

Operation
Execution
Result

Lazy evaluation

The operation is recorded first.

Operation
Plan
More operations
Action
Optimization
Execution

Spark primarily uses the second approach for transformations.


14. A Practical Example

Let’s consider a real-world pipeline.

Suppose you have a large transaction table:

transactions = spark.read.parquet(
"transactions/"
)

Then:

us_transactions = transactions.filter(
transactions.country == "US"
)
large_transactions = us_transactions.filter(
us_transactions.amount > 1000
)
customer_sales = (
large_transactions
.groupBy("customer_id")
.sum("amount")
)

At this point, Spark has not necessarily performed the complete computation.

Now:

customer_sales.show()

The action triggers execution.

Spark can consider the complete chain:

Read Transactions
Filter US
Filter Amount > 1000
Group By Customer
Sum Amount
Show

This complete view gives Spark an opportunity to optimize execution rather than blindly executing every statement independently.


15. Lazy Evaluation and Debugging

Lazy evaluation can sometimes confuse beginners.

You may write:

df = spark.read.csv("large_file.csv")
df = df.filter("age > 30")
print("Filter completed")

You might think:

Filter completed

means the filtering operation has already processed the entire dataset.

It doesn’t necessarily mean that.

The Python statement completed, but Spark’s distributed computation may not have been triggered.

If you want to force execution, you need an action such as:

df.count()

or:

df.show()

or:

df.write.parquet("output/")

16. A Common Interview Question

Question:

Is groupBy() an action or transformation?

Answer:

groupBy() is a transformation.

For example:

df.groupBy("department").count()

Here:

groupBy()

creates part of the transformation plan.

The final:

count()

is the action that triggers execution.

This distinction is important because groupBy() usually introduces a shuffle, which has significant performance implications.

We’ll explore that in detail when we cover DAGs, stages, and shuffle.


17. Another Interview Question

Question:

Does Spark execute every transformation separately?

No.

Spark builds an execution plan from the transformations and can optimize and pipeline compatible operations.

For example:

df.filter(...)
.select(...)
.withColumn(...)

doesn’t necessarily mean:

Execute filter
Write intermediate data
Execute select
Write intermediate data
Execute withColumn

Instead, Spark can often combine operations into an efficient execution plan.


18. The Most Important Takeaway

Lazy evaluation is not simply:

“Spark waits.”

The more important idea is:

Spark waits to execute the computation so that it can understand the larger computation and optimize how that computation should be performed.

The overall mental model should be:

Transformations
Build a plan
More transformations
Action
Optimize plan
Execute

This is one of the fundamental ideas behind Spark’s ability to efficiently process large datasets.


Continue Learning PySpark

You now understand lazy evaluation and why Spark doesn’t immediately execute every transformation.

The next step is to understand what Spark actually builds from those transformations.

Recommended next steps

1. DAG in Apache Spark
Understand how Spark represents your computation as a Directed Acyclic Graph.

2. Stages and Tasks
Learn how Spark breaks a DAG into stages and distributes work across executors.

3. Narrow vs Wide Transformations
Understand why some operations stay within a partition while others trigger a shuffle.

4. Spark Shuffle
Learn why joins and aggregations can become expensive.

View Complete Apache Spark Learning Path →


Leave a Reply

Discover more from Geeky Codes

Subscribe now to keep reading and get access to the full archive.

Continue reading