Four orders are paid: 101, 102, 104 and 106. Customer 1 has 40.00 + 25.00 = 65.00. Row order can change from run to run.
The filter is a transformation, so it runs nothing. Each action starts a job.
Now look at what each job did.
Job
Action
Reads the source files?
Job 0
count()
Yes
Job 1
show()
Yes, again
Spark numbers jobs from 0 in the Spark UI. One action can start more than one job. We show one job for each action to keep it simple.
Job 1 did not know that job 0 had just read and filtered the same rows. It started from the source.
With 6 rows this costs nothing. With 600 million rows, the second read is real work.
03 · Keep the result
cache() and reuse
⏱ 6 min · Topic 3 of 11
cache() tells Spark to keep a DataFrame, so later actions can reuse it. Spark keeps it in memory first, and on disk if memory is full. cache() is lazy, so calling it runs nothing.
The output is the same as before. Only the work behind it changed.
The first line only asks Spark to cache paid. It stores nothing yet. The count() fills the cache with the 4 paid rows. The show() reads them from memory.
Compare the two jobs with the first example.
Job
Action
Reads the source files?
What it does
Job 0
count()
Yes
Reads, filters, and stores the 4 rows in memory
Job 1
show()
No
Reads the 4 rows from memory
The first action still does all the work. Only the actions after it save work. So a cache helps only when at least two actions use the DataFrame.
Watch out
cache() alone stores nothing. If no action runs, the cache stays empty. Add an action, such as count(), when you want the cache ready.
04 · Where to keep it
Storage levels with persist()
⏱ 4 min · Topic 4 of 11
A storage level says where Spark keeps the cached data. persist() lets you choose the level. cache() on a DataFrame uses MEMORY_AND_DISK.
Partitions that do not fit are not stored. Spark computes them again when needed.
MEMORY_AND_DISK
Memory first, then disk
Partitions that do not fit go to disk.
DISK_ONLY
Disk only
Not a problem, but reads are slower than memory.
Spark has more levels, such as copies on two executors. These three are enough to start.
Use the default unless you have a reason to change it. Newer Spark versions may show the default as MEMORY_AND_DISK_DESER. It means the same thing: memory first, then disk.
Choose the level before the first action. If the DataFrame is already cached, persist() with a new level changes nothing. Call unpersist() first.
In the chapter "Spark Architecture: Driver, Executors & Cluster Managers", executor memory has two parts. Cached data lives in storage memory. Execution memory is for work like joins, grouping and sorting.
05 · Clean up and check
unpersist() and the Storage tab
⏱ 5 min · Topic 5 of 11
When you finish with a cached DataFrame, remove it. unpersist() frees the memory and the disk space.
Release the cacheworked example
SQL
paid.unpersist()
Effect
Before unpersist()
After unpersist()
4 paid rows kept in memory
Nothing kept. The next action reads the source again.
Later actions on paid still work. They just compute it again.
To see what is cached, open the Spark UI and click the Storage tab. It has one row for each cached DataFrame.
Each row has these columns: Storage Level, Cached Partitions, Fraction Cached, Size in Memory and Size on Disk. If Fraction Cached is below 100%, some partitions are not cached yet. For example, show() may read only the first partitions.
If a DataFrame is missing from the Storage tab, no action has filled it yet. Or you already called unpersist().
06 · Good judgment
When not to cache
⏱ 5 min · Topic 6 of 11
A cache costs memory. Executors need that memory for joins and shuffles. Do not cache in these three cases.
The DataFrame is used by only one action. Nothing reads the cache, so you only pay.
The DataFrame is much bigger than storage memory. Most partitions go to disk, and reading them can cost as much as reading the source.
The DataFrame is cheap to compute. Reading a small file again can cost less than the cache.
A common mistake is to cache too early. Compare two ways to cache the paid orders.
After 3 rounds the plan has 3 steps. After 50 rounds it has 50.
A long lineage makes the plan slow to build. It also means a lost partition is slow to rebuild, because Spark replays every step.
A cache does not fix this. A cached DataFrame still keeps its lineage. You need a checkpoint.
08 · Cut the plan
Checkpoint cuts lineage
⏱ 7 min · Topic 8 of 11
A checkpoint saves the data to files and forgets the steps before it. After that, the lineage starts at the saved files. First you set the folder where Spark saves them.
Lineage at some loop steps (i is the loop counter)
Loop step
Steps Spark remembers
i = 9, after checkpoint()
read the checkpoint files
i = 10
read the checkpoint files, step 1
i = 18
read the checkpoint files, step 1 to step 9
i = 19, after checkpoint()
read the checkpoint files
The list starts again at the files after every checkpoint.
This loop checkpoints every 10 rounds, so the plan never holds more than 10 steps after the files.
For a DataFrame, checkpoint() is eager by default. It runs a job right away to write the files.
This is different from cache(), which is lazy.
There is also localCheckpoint(). It cuts the lineage too, but it keeps the data on the executors, not in reliable storage.
If an executor is lost, the data is lost. It skips the write to the checkpoint folder, but it is less safe.
Spark does not delete checkpoint files by default. Clean up the folder when the job is done.
cache()
checkpoint()
localCheckpoint()
Where the data goes
Executor memory or disk
Checkpoint folder
Executor storage
Cuts the lineage?
No
Yes
Yes
Safe if an executor is lost?
Spark can rebuild it from the lineage
Yes
No
Interview question
The question: "When do you use checkpoint instead of cache?" A good answer is short. "When the lineage is long, for example in a loop. Checkpoint writes the data to reliable storage and cuts the lineage."
09 · Reuse without a cache
Shuffle files are reused
⏱ 3 min · Topic 9 of 11
You know that Spark keeps shuffle files on local disk. A later job on the same DataFrame can read those files and skip that stage. The Spark UI then marks that stage as skipped.
So some reuse happens without a cache. Do not plan for it. The files can be deleted, and it only covers the shuffle, not the work before it.
10 · Recap
Check yourself
⏱ 4 min · Topic 10 of 11
Four points to keep.
Each action runs the plan again. A cache keeps the result, so the next action does not read the source.
cache() is lazy. The first action fills it, and unpersist() removes it.
Do not cache data used once, data bigger than memory, or data that is cheap to compute. Cache after the filter.
A loop makes the lineage grow. checkpoint() saves the data and cuts the lineage. A cache does not.
Answer these without looking back.
In the paid-orders example, which jobs read the source when there is no cache?
Why does cache() alone store nothing?
Which storage level keeps partitions on disk when memory is full?
Why does a cache not fix a long lineage?
Which is safer if an executor is lost: checkpoint() or localCheckpoint()?
11 · Next Chapter
Next Chapter
You can now keep a result, and cut a long plan.
⏱ 3 min · Topic 11 of 11
Next chapter
Broadcast Joins, Shared Variables & Bucketing
You can now keep a result, and cut a long plan.
Broadcast joins and bucketing help Spark avoid a shuffle.