Spark: RDDs, transformations and actions

After this lesson you will explain why one engine replaced three stacks, define an RDD word by word, tell transformations from actions and narrow from wide dependencies, and follow a Spark job from the driver to its tasks — and back from a lost partition.

The job example of the lecture loads a log from HDFS, keeps the error lines, cuts out the message and caches it: four lines of Python. Each of the four returns at once. Not because Spark is fast — Spark has not read a single byte yet.

Then come the questions: how many messages mention mysql, how many php. Next to the code the slide prints a measurement on 60 GB of Wikipedia text spread over 20 machines: each search takes 0.5 seconds on the data kept in memory, against 20 seconds on disk (p. 1222).

Now one of the machines dies. Its share of the cached data is gone, and Spark kept no copy of it anywhere. The next count is still right. Two puzzles for the end of the lesson: what were the four lines doing, if not computing? And where does lost data come back from, if nobody copied it?

One engine instead of three stacks

What is big data used for (p. 1154)? Reports track business processes; diagnosis asks why engagement drops or the system is slow, and detects spam, worms and DDoS attacks; decisions pick a treatment, a product feature, an ad. The slide's yellow box: "Data is only as useful as the decisions it enables".

Faster and better decisions set three processing goals (p. 1155): low-latency interactive queries on historical data ("identify why a site is slow and fix it"); low-latency queries on live data, streaming ("a worm may infect 1mil hosts in 1.3sec"); and sophisticated processing such as anomaly detection — iterative algorithms, machine learning, graphs. The next slide draws batch, interactive and streaming as the corners of a triangle around the words "Single Stack!": "Support batch, streaming, and interactive computations… … and make it easy to compose them" (p. 1156).

Why compose them? The DDoS example of p. 1158 needs all three at once. To decide whether a traffic surge is an attack you must "Detect attack pattern in real time" (streaming), ask "Is traffic surge expected?" (an interactive query on history), and that query is fast only thanks to "pre-computation (batch)" — "And need to implement complex algos (e.g., ML)!"

The slide's "Today's state-of-art analytics stack" (p. 1157) takes three systems: a demultiplexer copies the logs into a streaming stack (Storm, lesson 33) and a batch stack (Hadoop, lesson 31), which feeds an interactive store (HBase, Impala, lesson 32). The price, as the slide lists it: three stacks to maintain, "Expensive and complex"; "Hard to compute consistent metrics across stacks" — the same "errors per minute" coded twice; and "Hard and slow to share data across stacks", since the data moves between them as files.

logs demux Storm streaming stack Hadoop batch stack copy HBase, Impala interactive queries 3 stacks, 3 copies

Before: three stacks (p. 1157)

Spark Streaming Spark SQL MLlib GraphX Spark: one engine same workers, same cache HDFS, S3, Cassandra, … logs 1 engine, 1 copy

After: one engine (pp. 1161, 1199)

Fig. 1. Before: three stacks, three code bases and three copies of the data, with files moving between them. After: one engine whose libraries share the same workers and the same cached data.

Spark's answer is one general engine with libraries on top, in a stack of three layers (p. 1159): data processing, resource management, storage. The Berkeley Data Analytics Stack (p. 1161) fills them with Spark and its libraries, Mesos to share the machines, and HDFS or S3 under Tachyon, an in-memory storage layer. Spark owns only the top layer: "You can think of Spark as scalable computation platform", one that uses data stored in Cassandra, AWS S3, HDFS "And more" (p. 1201). It keeps nothing permanently: its input and its results live in those systems. Table 1 lines up the three drawings of the stack: the names change, the layers do not.

Table 1. One stack, three slides; the layers are those of p. 1159. The mind map of p. 1162 makes the same point with logos: Spark in the middle of six workloads — streaming, ETL, machine learning, analysis, graph processing, data lake — each ringed by the tools it would otherwise take (Storm, Flink, Pig, scikit-learn, Mahout, pandas, Jupyter, Giraph, Neo4j, Hive).
LayerBerkeley stack (p. 1161)Spark in context (p. 1202)Spark components (p. 1203)
Data processing: librariesSpark Streaming, SparkSQL, GraphX, MLlib; BlinkDB, MLPipelinesSpark SQL, Spark Streaming, MLlib, GraphXStructured Streaming, Spark ML, GraphFrames, over DataFrames / Datasets / SQL APIs, over the RDD API
Data processing: engineSparkSpark (General execution engine)Spark Core
Resource managementMesosYarn / Mesos (optional); Apache ZooKeeper"Environments": YARN, Mesos, Kubernetes, Docker, EC2, OpenStack, Databricks
StorageHDFS, S3, … with Tachyon on topHadoop Filesystem (HDFS)"Data Sources": HDFS, Cassandra, Hive, HBase, PostgreSQL, MySQL, Elasticsearch, S3, Parquet, CSV, JSON, Kafka

As the slides tell it (p. 1194), Spark began in 2009 as a research project of Matei Zaharia at the UC Berkeley AMPLab, went to the Apache Software Foundation in 2013, became a top-level Apache project in February 2014, and in November 2014 set a world record in large-scale sorting for Databricks, Zaharia's company.

It promised to be "Fast: up to 100x faster than Hadoop", "Easy to use: 5-10x less code than Hadoop" and "General: support interactive & iterative apps" (p. 1195). The 100x has fine print. Two slides later the same kind of claim, made for Shark against Hive, comes in two sizes: "Up to 100x when input is in memory", "Up to 5-10x when input is on disk" (p. 1197). Fig. 2 shows where the numbers come from.

Interactive SQL query seconds: less is better Hive ≈ 62 Impala ≈ 17 Shark ≈ 1 Machine learning, batch seconds per iteration Hadoop ≈ 110 Spark ≈ 1 Streaming MB/s per node: more is better Storm ≈ 12 Spark Streaming ≈ 26
Fig. 2. The three charts of p. 1198, "Performance and Generality", redrawn as horizontal bars; the values are read off the slide's chart, not printed on it. Teal: the systems of the three stacks; magenta: the Spark family. The first two charts measure time, the third throughput.

Why is the iterative bar the most dramatic? An iterative algorithm reads the same data at every pass. The logistic regression of p. 1199 — the gradient descent of machine learning — computes one gradient term per point with points.map(...), sums the terms with .reduce(_ + _) and repeats. On Hadoop each pass is a new MapReduce job that reads its input from HDFS and writes its output back; Spark reads the points once, keeps them with features.cache(), and every further pass runs in memory. The features come from a SQL join on the same machines: the libraries "All share the same set of workers and caches".

The libraries are the old stacks rebuilt on the engine. Spark Streaming will "Implement streaming as a sequence of <1s jobs" (p. 1196): small batches, each an ordinary Spark job, so the stream inherits fault tolerance and straggler handling and counts every record exactly once (the slide's "exactly one semantics"; its last line prints "batch" twice where it means batch, interactive and streaming). The slide titled SparkSQL describes Shark, "Hive over Spark: full support for HQL and UDFs" — its box is the highlighted one; Spark SQL replaced it (lesson 40). And each layer speaks an existing interface (p. 1200): Kafka, Flume, Twitter and TCP sockets feed Spark Streaming; Shark keeps the Hive API, GraphX the GraphLab API; Mesos also runs Hadoop, Storm and MPI; Tachyon offers the HDFS API.

"Spark replaces Hadoop." It replaces MapReduce, the engine. Spark reads HDFS through Hadoop's InputFormat API and can run on YARN (p. 1220) — which is why the deck repeats, on pp. 1163–1192, the HDFS and YARN slides of lecture 11 one for one. They are taught in lesson 31.

Spark is a computation engine, not a store: one engine and its libraries replace three stacks, and the data stays in the workers' memory between the steps.

RDDs: a recipe, partitions and a cache

Here is the job of p. 1222, the one from the opening, as you would type it today. It will "Load error messages from a log into memory, then interactively search for various patterns":

# sc is the SparkContext of your program (sc = spark.sparkContext)
lines = sc.textFile("hdfs://...")
errors = lines.filter(lambda s: s.startswith("ERROR"))
messages = errors.map(lambda s: s.split("\t")[2])
messages.cache()
# two actions: each returns one number to the driver
messages.filter(lambda s: "mysql" in s).count()
messages.filter(lambda s: "php" in s).count()

Read it line by line. textFile returns an RDD of lines, one partition per HDFS block — and reads nothing. filter returns a new RDD of the error lines, map one that keeps the third tab-separated field: still nothing runs. cache() only marks messages to be kept in memory once computed. The first count() starts the work (Fig. 3): one task per block; each worker reads its block, filters, maps, caches its partition of messages and counts the "mysql" lines; the three numbers are added up at the driver. The second count() starts from the caches: no disk, no filter, no map.

driver SparkContext tasks results worker 1 block 1 cache 1 worker 2 block 2 cache 2 worker 3 block 3 cache 3 only numbers travel back
Fig. 3. The first count() of the job of p. 1222: one task per HDFS block. Each worker filters and maps its own block, keeps its partition of messages in its cache and sends back one number. The second count() reads the three caches only.

"Reads nothing" is literal. On PySpark 4.2.0, textFile on a path that does not exist, followed by a filter and a map, raises no error at all; "Input path does not exist" arrives only with count().

Resilient Distributed Dataset (RDD) — Spark's core abstraction: an immutable collection of records split into partitions spread over the machines of a cluster, built from stable storage or from other RDDs by deterministic transformations, evaluated lazily, cacheable, and fault-tolerant because Spark remembers how every partition was made.

The slides give it "4 main features" (p. 1205): "Distributed Collection of Data", "Fault-tolerant", "Parallel operation - partioned" and "Ability to use many data sources"; and three adjectives: "immutable, lazily evaluated, cacheable" (p. 1208). Each word of the name is a mechanism. Resilient: a lost partition is recomputed, not restored from a copy. Distributed: the partitions sit on many machines and are processed in parallel, one task each. Dataset: you program it as one collection.

Its operations come in two kinds (p. 1209): "Transformations are a recipe to follow. Actions perform what the recipe says to do and returns something back." A transformation builds a new RDD "through deterministic operations on other RDDs" — map, filter, join — and is a "lazy operation"; an action will "return value or export data" — count, collect, save — and "triggers execution" (p. 1211). Table 2 collects the lists of the slides.

Table 2. Transformations and actions, as the slides list them.
WhereTransformations (lazy): return a new RDD or DataFrameActions: start a job, return a value or write data
RDD operations (p. 1211)map, filter, joincount, collect, save
"Operations" table (p. 1212), DataFramesselect, distinct, groupBy, sum, orderBy, filter, limitshow, count, collect, save
Exercise session (p. 1722), RDDsmap(), filter(), flatMap(), distinct(), union(), reduceByKey(), groupByKey(), sortBy()collect(), count(), take(n), reduce(), saveAsTextFile(), first()

To find the lines that start a job, look at what each call returns. A new RDD or DataFrame: a transformation, nothing ran. A number, a list, a printed table or a file: an action, a job ran.

Why wait? "With a large dataset, you don't want to calculate all the transformations until you are sure you want to perform them!" (p. 1210). Laziness pays three times: nothing is computed that no action needs; consecutive transformations such as filter and map run as one pass over each partition, record by record, without storing the RDD in between; and Spark sees the whole plan before it runs anything, so it can cut it into stages and send the work to where the data is.

"RDD are immutable, transformations create new RDD" (p. 1213). So an RDD stays what it was born as — a parent and a function — and the chain of those, back to the file, is its lineage (Fig. 4). Spark will use the lineage to rebuild lost data, so a transformation must give the same result every time it runs: a function that drew random numbers or read the clock would rebuild different data. Immutability pays a second time in the cache: since nobody can change a cached partition, many jobs can read it at once without locks.

filter map HDFS file filtered RDD mapped RDD startswith(…) split(…) lineage
Fig. 4. The lineage of p. 1213, repeated on p. 1223: each transformation creates a new RDD and records its parent and its function. Nothing is stored in between; the chain itself is what Spark keeps.

Transformations write the recipe, actions cook it: until an action, an RDD is a lineage of operations, not data.

Narrow and wide: does the data have to move?

Take the readings of the heating systems of three hospitals: a pair RDD of (hospital, kWh) in three partitions. filter keeps or drops each record where it lies, so output partition 1 is built from input partition 1 alone, on the same machine, in the same task. reduceByKey adds up the kWh of each hospital, so all the records of H1 must meet in one partition — and they lie in partitions 1 and 3. Spark cannot know in advance which hospitals a partition holds, so every input partition is cut by key into one piece per output partition, and every output partition fetches its piece from every input partition: a shuffle.

The slide of p. 1214 names the two cases. Narrow transformations, "1 to 1": filter(..), drop(..), coalesce(). Wide transformations, "(shuffles) 1 to N": distinct(), groupBy(..).sum(), repartition(n). Its pictures count from the input side: one input partition to one output, one input partition to N outputs. Its definitions count from the output side, and the narrow one gets it wrong.

Narrow or wide? Follow the arrows between partitions

Readings of three hospitals' heating systems, (hospital, kWh), in three partitions on the left; the result of the chosen operation on the right. An arrow means "this output partition depends on that input partition": Spark reads from it to build the output partition. After a shuffle each hospital lands in its own output partition: an idealised hash partitioning.

  1. Start with map, then filter. Before you look: does output partition 2 need anything from input partitions 1 or 3? Then switch to groupByKey.
    What you should see

    For map and filter, one arrow per output partition, from the input partition in the same place: each partition is computed where it lies, nothing moves, 0 values shuffled. For groupByKey every output partition has an arrow from every input partition — the records of H1 lie in p1 and p3 — and all 9 kWh values go through the shuffle. That is the one thing this widget makes impossible to miss: wide means an input partition is split among many outputs, a shuffle, whatever the operation is called.

  2. Choose coalesce(2). How many input partitions feed output p2? By the sentence printed on p. 1214, narrow or wide? Then read the verdict.
    What you should see

    Two: p2 and p3 go, whole, into output p2. By the printed sentence that would be wide ("many partitions of the parent RDD"); yet each input partition feeds exactly one output, nothing is split or regrouped, 0 values shuffled: narrow, as the slide's own example list says. Count from the input side.

  3. Compare groupByKey and reduceByKey. Predict which one moves fewer values through the shuffle, and why both give the same totals.
    What you should see

    The arrows are the same — both are wide — but reduceByKey shuffles 6 partial sums instead of 9 values: each input partition first adds up its own values per hospital (p1 sends H1 7 and H2 3), like the combiner of MapReduce. The totals are H1 11, H2 10, H3 9 either way.

  4. Choose join(hospitals), then switch on "inputs already partitioned by hospital". What happens to the arrows and to the shuffle?
    What you should see

    Off: every output partition reads every partition of both inputs, and all 12 records (9 readings, 3 hospitals) are shuffled. On: partition i of the readings meets partition i of the hospitals, one arrow each, nothing shuffled. The same join is narrow when the data already sits where the result needs it — getting it there cost one shuffle per input, once.

The difference decides three things. Cost: a shuffle writes the pieces to the senders' local disks and pulls them over the network — the shuffle of MapReduce (lesson 30). Scheduling: an output partition after a shuffle needs pieces from all the input partitions, so every input task must finish first; the job is cut there, while narrow steps are chained inside one task. Recovery: a lost narrow partition is rebuilt from its own input partition, a lost partition after a shuffle from pieces of all of them.

The same operation can be either. A join is wide when Spark does not know where each key lives; when both inputs are already partitioned by the join key with the same partitioner, partition i of one meets partition i of the other and nothing moves: narrow, as the RDD paper of 2012 notes (and as PySpark 4.2.0 confirms).

Narrow: each input partition feeds at most one output partition, and nothing moves. Wide: an input partition is split among many outputs — a shuffle, and a new stage.

From RDDs to DataFrames

"With Spark 2.0, Spark is moving towards a DataFrame based syntax. But the way files are being distributed can still be thought of as RDDs, it is just the typed out syntax that is changing" (p. 1215). A DataFrame is "Immutable Data with named columns (built on RDDs)", with a "User-friendly API", "Uniform APIs across languages (Scala, Java, Python, R, and SQL)" and "Improved performance via optimizations (Tungsten and Catalyst)" (p. 1216) — the execution layer and the optimizer of lesson 40. Partitions, laziness, lineage, narrow and wide: everything above still holds.

The timeline of p. 1204 says what changed. An RDD (2011) is a "Distribute collection of JVM objects" with "Functional Operators (map, filter, etc.)": Spark runs your functions but cannot look inside them. A DataFrame (2013) is a "Distribute collection of Row objects" with "Expression-based operations and UDFs" and "Logical plans and optimizer": Spark can read col("age") > 30, optimize the plan and store the rows compactly. A Dataset (2015) is "Internally rows, externally JVM objects": typed and fast, "But slower than DF". The years are the slide's; the DataFrame API itself shipped with Spark 1.3, in 2015. Since Spark 2.0 they are one API (p. 1219): "DataFrame = Dataset[Row]" is the untyped alias, "Dataset[T]" the typed one, for Scala and Java. Python has only DataFrames — all the exam needs.

The code example of p. 1217 queries "different data sources" two ways: register DataFrames as temporary views and write a SQL string, or chain DataFrame methods. Both build the same plan. Corrected, they read:

# route 1: register views, then a SQL string
jsonDf = spark.read.json("s3n://…")
jsonDf.createOrReplaceTempView("json")
peopleDf.createOrReplaceTempView("people")
results = spark.sql("""SELECT p.id, p.name, j.country
                       FROM people p JOIN json j ON p.id = j.id
                       WHERE p.age > 30""")
# route 2: the DataFrame API, the same plan
results = (peopleDf.join(jsonDf, peopleDf.id == jsonDf.id)
                   .filter(peopleDf.age > 30)
                   .select(peopleDf.id, "name", "country"))

The paper of 30 Jan 2026 accepts only the second route: "Note that you are not allowed to use SQL syntax. Use PySpark instead." Lesson 39 trains it on every past SparkQL exercise.

Which is faster? Fig. 5 redraws the chart of p. 1218, "Time to aggregate 10 million integer pairs (in seconds)". RDD code depends on the language — Python about 9.5 s, Scala about 4.2 s — because Python lambdas run in separate Python processes, every record serialized to reach them. DataFrame code does not: about 2 s in SQL, R, Python and Scala alike, because the API only describes the computation and the optimized plan runs inside the JVM whatever language wrote it.

DataFrame SQL ≈ 2.1 DataFrame R ≈ 2.1 DataFrame Python ≈ 2.05 DataFrame Scala ≈ 2.1 RDD Python ≈ 9.5 RDD Scala ≈ 4.2 0 2 4 6 8 10 seconds to aggregate 10 million integer pairs
Fig. 5. The chart of p. 1218 ("Chart from Databricks"), values read off the slide. Magenta: DataFrame code, about 2 s in every language. Teal: RDD code, whose time depends on the language.

That is the fine print of "Available APIs" (p. 1226): "You can write in Java, Scala or Python", with an "interactive interpreter" for "Scala & Python only", and "Java & Scala are faster thanks to static typing" — true of RDD code, where Python pays for its separate processes; for DataFrames the chart shows no gap. (R and SQL, listed on p. 1216, are missing here.)

A DataFrame is an RDD of rows with named columns: the same partitions and laziness, plus a plan that Spark itself can read and optimize.

The life of a job — and of a lost partition

"Spark runs as a library in your program (1 instance per app)" (p. 1220): in the architecture of the slides you do not send queries to a Spark server, as you would to a database. Your program is the driver. Its one SparkContext runs tasks in local threads or obtains executors — processes on the worker machines — from a cluster manager, in "Mesos, YARN or standalone mode"; the tasks read storage themselves, "via Hadoop InputFormat API" (HBase, HDFS, S3). Fig. 6 draws who talks to whom (pp. 1206, 1220); the "Master" and "Slave" of p. 1207 are the driver and the executors.

tasks and results driver your program SparkContext cluster manager Mesos, YARN or standalone starts executors worker node executor cache task task shuffle worker node executor cache task task HDFS or other storage
Fig. 6. Who talks to whom (pp. 1206, 1220). The cluster manager only hands out executors. Tasks and results travel directly between the driver and the executors, shuffle data from executor to executor, and every executor reads the storage itself.

What runs on the executors? The slide of p. 1224 shows the job of p. 1221 (log, then errors) at two levels. The dataset-level view shows objects: log is a HadoopRDD with path = hdfs://..., errors a FilteredRDD with func = _.contains(…) and shouldCache = true, the flag that cache() sets. The partition-level view shows the work: four partitions of log above four of errors, and one task per column — Task 1, Task 2, … — that reads its partition and filters it in one pass. A stage runs one task per partition.

The slide of p. 1225 follows an action through four components (Fig. 7). The transformations, rdd1.join(rdd2).groupBy(…).filter(…), have built a DAG of RDD objects. The action hands it to the DAGScheduler, which will "split graph into stages of tasks" and "submit each stage as ready": a stage ends wherever a wide dependency begins. Each stage goes, as a TaskSet of one task per partition, to the TaskScheduler, which will "launch tasks via cluster manager" and "retry failed or straggling tasks". On each worker, threads "execute tasks" and the block manager will "store and serve blocks": cached partitions and shuffle output. The words to keep: a job is everything one action triggers, a stage is a piece of it without shuffles, a task is one stage on one partition.

RDD objects build the operator DAG rdd1.join(rdd2) .groupBy(…) .filter(…) rdd1 rdd2 join groupBy filter DAG DAGScheduler split the graph into stages submit each stage when ready stage 1 stage 2 TaskSet TaskScheduler launch tasks, retry failed or straggling ones cluster manager task worker execute tasks store and serve blocks threads block manager
Fig. 7. Job scheduling (p. 1225), top to bottom: the transformations have built a DAG of RDD objects; the DAGScheduler cuts it into stages at the shuffle (the crossing arrows, on the highlighter) and hands each stage on as a TaskSet. The DAGScheduler panel is the slide's sketch, with one shuffle: how many stages the real job gets depends on how the data is already partitioned. Then the TaskScheduler launches the tasks through the cluster manager and retries failed or straggling ones; on each worker, threads run the tasks and the block manager keeps cached partitions and shuffle output.

"RDDs track lineage information that can be used to efficiently recompute lost data" (p. 1223). When a worker dies, the partitions it held in memory are gone. The driver looks up their lineage — the HDFS file, filter and map of Fig. 4 — and schedules tasks on other workers that re-read the matching blocks, which HDFS still keeps on other DataNodes, and re-apply the functions, for the lost partitions only. Nothing is copied while all goes well, and recovery costs in proportion to what was lost. HDFS protects stored blocks by replication; Spark protects computed partitions by lineage. (Background, from the RDD paper: when a lineage grows very long, an RDD can be checkpointed to stable storage to cut it.)

Build a lineage, then run it

A log of the heating systems of three hospitals: 12 lines in 3 HDFS blocks, one block per worker — an idealised cluster, the chain of the exercise session's example (p. 1723). Add transformations: each adds a row of partitions and runs nothing. An action runs a job: the partitions it computes or reads are highlighted, shuffle files appear at the stage boundary, cached partitions stay filled. Then make worker 2 fail and run an action again. The numbers are those of the same program run on PySpark 4.2.0.

  1. Press + filter, + map and + reduceByKey. Before each press, predict "lines read from HDFS, in total". Then press count().
    What you should see

    The total stays at 0: three transformations, three rows of empty, dashed partitions, no job. count() runs job 1 in two stages, cut where the arrows cross: stage 1 has 3 tasks that each read a block, filter and map it in one pass and write a shuffle file; stage 2 has 3 tasks that fetch those files and add up. 12 lines read, count() = 3, one record per hospital. That is the one thing this widget makes impossible to miss: nothing runs until an action, and the job splits exactly where the data must move.

  2. Press count() again, without caching anything. Predict the lines this job reads.
    What you should see

    0, with stage 1 skipped: its output, the shuffle files, is still on the workers' disks, so only the 3 tasks of stage 2 run (PySpark does the same); the total stays at 12. Without a shuffle there is nothing to reuse: start over, + filter, then count() twice reads 12 lines both times.

  3. Start over. Build filter and map, press cache(), then collect() twice. Then add reduceByKey and collect() once more. Predict the lines read each time.
    What you should see

    12, then 0: the first action filled the cache of pairs, the second read memory only. After reduceByKey still 0 — stage 1 now starts from the cached pairs, not from HDFS: the interactive pattern of p. 1222.

  4. Now press worker 2 fails. Predict what is lost and what the next count() recomputes.
    What you should see

    Lost: the cached partition 2 of pairs and the shuffle file of worker 2 (crossed out). The next count re-reads only block 2 — 4 lines — from another HDFS replica, re-applies filter and map to partition 2 (it is cached again), reruns that one map task and then the 3 reduce tasks. Partitions 1 and 3 are not touched: their shuffle files are still on workers 1 and 3. The lineage rebuilds exactly what was lost.

Back to the puzzle of the opening. The four lines computed nothing: each added a node to the lineage, and cache() only set a flag. The first count ran the recipe and left each worker's partition of messages in its memory — hence half a second per search instead of a pass over the disk. The lost partition came back from the same recipe: one task re-read its block from a surviving HDFS replica and re-applied filter and map. Nobody had copied it.

An action becomes a job; the DAGScheduler cuts it into stages at the shuffles; each stage runs one task per partition; a lost partition is recomputed from its lineage.

Exam-style questions

Short open answers and multiple choice, the forms of the theory part, where a wrong answer may cost points; then one exercise worded like the papers' Spark exercises, but on RDDs, the subject of this lesson. The model answers are ours.

What is an RDD in Spark? Explain the meaning of each word of the name and the main properties of RDDs, and say why Spark evaluates transformations lazily.

Show model answer

An RDD (Resilient Distributed Dataset) is the core abstraction of Spark: an immutable collection of records split into partitions that are spread over the nodes of a cluster and processed in parallel. It is created from data in stable storage, such as a file in HDFS, or from other RDDs through deterministic transformations such as map, filter or join. Resilient: it is fault-tolerant, because Spark records its lineage (the parent RDDs and the function applied) and recomputes lost partitions instead of replicating them. Distributed: its partitions live on different worker nodes, and each partition is processed by one task. Dataset: the programmer handles it as one collection of data. RDDs are immutable (a transformation returns a new RDD), lazily evaluated (transformations only build the lineage; an action such as count or collect triggers the computation) and cacheable (cache() keeps the computed partitions in the executors' memory for later actions). Laziness lets Spark see the whole chain before running it: nothing is computed that no action needs, consecutive narrow transformations run in one pass over each partition, and the plan can be cut into stages and sent to where the data is.

The examiner looks for the three words tied to their mechanisms — lineage, partitions — and the three adjectives of p. 1208; for "why lazy", the reason the slide gives on p. 1210: with a large dataset you do not compute transformations until you are sure an action needs them. "Resilient because it is replicated" is the classic wrong answer.

Which of the following Spark operations are actions? Select all that apply.

  • count() It returns the number of elements to the driver and triggers execution (p. 1211).
  • map() A transformation: it returns a new RDD and runs nothing.
  • collect() It returns all the elements to the driver: an action (pp. 1211–1212).
  • reduceByKey() A (wide) transformation: it returns a new RDD of pairs. Do not confuse it with reduce(), which is an action.
  • saveAsTextFile() It exports the data to storage: the "save" of p. 1211.
  • filter() A transformation: a new RDD with the records that pass the test.
Show model answer

count(), collect() and saveAsTextFile(). The others are transformations: they return a new RDD and are only executed when an action is called.

A select-all question where a wrong tick may cost points. The trap is reduceByKey: it sounds like reduce, but it returns an RDD.

Which statement about Spark DataFrames is correct?

  • A DataFrame can be modified in place, row by row. DataFrames are immutable: every operation returns a new DataFrame (p. 1216).
  • A DataFrame is an immutable distributed collection with named columns, built on RDDs, whose operations Spark can optimize. The definition of p. 1216, with "Improved performance via optimizations (Tungsten and Catalyst)".
  • DataFrames are available only in Scala and Java. The slide of p. 1216 says "Uniform APIs across languages (Scala, Java, Python, R, and SQL)".
  • A DataFrame query in Python is about twice as slow as the same query in Scala. That holds for RDD code (about 9.5 s against 4.2 s on the chart of p. 1218); DataFrame code takes about 2 s in every language.
Show model answer

The second: a DataFrame is immutable data with named columns, built on RDDs, with a uniform API across languages and performance improved by the Catalyst optimizer and Tungsten.

Each wrong option is a known confusion: the pandas habit of editing in place, the typed Dataset of p. 1219 (Scala and Java only), and "Python is slow", which is true only of RDD code.

Explain the difference between narrow and wide transformations in Spark, give two examples of each, and explain how Spark uses this difference when it executes a job.

Show model answer

A transformation is narrow when each partition of the parent RDD is used by at most one partition of the child RDD: every output partition can be computed from its input partition(s) on the same node, without moving data. Examples: map, filter, union, coalesce. It is wide when a partition of the parent may be needed by many partitions of the child, so the records must be regrouped by key across the cluster with a shuffle. Examples: groupByKey, reduceByKey, distinct, repartition, or a join whose inputs are not co-partitioned. When an action is called, the DAGScheduler splits the DAG of RDDs into stages at the wide dependencies. Inside a stage the narrow transformations are pipelined into one task per partition; a stage that follows a shuffle can start only when all the tasks of its parent stage have written their shuffle output. Wide dependencies are therefore more expensive (disk and network), and a lost partition after a shuffle may need data from all the parent partitions.

The slide prints the narrow definition counted from the child (see the red pen above); writing the standard direction, or adding coalesce as an example, shows that you understand it. The second half, stages cut at shuffles, is what makes the answer complete.

A worker node that holds cached partitions of an RDD fails while a Spark application is running. How does Spark recover the lost data? Compare this mechanism with the way HDFS protects data.

Show model answer

Spark does not keep copies of RDD partitions. Every RDD records its lineage: its parent RDDs and the deterministic transformation that produced it, down to the data in stable storage. When a worker fails, the driver uses the lineage to recompute only the lost partitions: it schedules tasks on the surviving workers that re-read the corresponding input blocks, which HDFS still holds on other DataNodes, and re-apply the transformations, for example filter and then map; lost shuffle outputs are recomputed in the same way. The partitions cached on the other workers are not touched. HDFS instead protects stored data by replication: each block is written to three DataNodes by default, so the other copies cover a lost node. Lineage costs nothing while no failure happens and its recovery is proportional to the lost data, while replication multiplies the storage used. This is also why Spark transformations must be deterministic.

Three things are checked: lineage as the recipe, recomputation of the lost partitions only, and the contrast with replication. The follow-up thought — deterministic functions — is the bonus.

Consider the hospital heating management system of the exam of 30 Jan 2026: a Hospital (Hospital_ID, Name) has several Heating Systems (Heat_ID, H_KW_Cons, Heat_Power), as in the tables of its Spark exercise. Every heating system writes its events into one log file stored in HDFS, one event per line, in the form LEVEL<TAB>Heat_ID<TAB>message, where LEVEL is INFO, WARN or ERROR. (The log and the questions are ours, in the style of the papers.)

Write a Python code with PySpark RDDs that keeps the ERROR events in memory and then computes (a) how many ERROR messages contain the word "boiler" and (b) the number of ERROR events of each heating system. Say which lines start a Spark job.

Show model answer
from pyspark.sql import SparkSession
spark = SparkSession.builder.appName("HeatingLog").getOrCreate()
sc = spark.sparkContext                                            # the RDD API lives here

lines = sc.textFile("hdfs://.../heating.log")                      # nothing runs
errors = lines.filter(lambda s: s.startswith("ERROR"))             # transformation
errors.cache()                                                     # lazy: filled by the first action
n_boiler = errors.filter(lambda s: "boiler" in s.split("\t")[2]).count()   # action: job 1
per_system = (errors.map(lambda s: (s.split("\t")[1], 1))
                    .reduceByKey(lambda a, b: a + b)
                    .collect())                                    # action: job 2

Only the two actions start jobs: count() runs job 1, a single stage in which each task reads its block, keeps the ERROR lines, stores them in the cache and counts the matches; collect() runs job 2, whose first stage reads the cached errors instead of HDFS and whose second stage, after the shuffle of reduceByKey, adds up the events per Heat_ID. All the other lines only build the lineage.

Run on a six-line test log with PySpark 4.2.0, this code gives 2 boiler messages and the pairs ('HS_1', 2), ('HS_2', 1), ('HS_3', 1), in whatever order the partitions return them. What to get right: startswith in lower case, cache() before the first action, count and collect as the only lines that start a job, and reduceByKey rather than groupByKey. The papers' own Spark exercises use DataFrames: lesson 39 solves them.

Check yourself

You run lines = sc.textFile(...), errors = lines.filter(...) and messages = errors.map(...) on a 60 GB file. How much of the file has Spark read?

  • None of it Transformations only add nodes to the lineage. Even a wrong path is noticed only at the first action.
  • Only the first block, to check the format Spark does not even check that the file exists before an action.
  • Only the error lines To find the error lines it would have to read everything — and it has not started.
  • All of it, but in the background There is no background work: nothing runs until an action asks for a result.

messages.cache() is followed by two count() calls on filters of messages. When are the partitions of messages stored in memory?

  • When cache() is called cache() is lazy: it only marks the RDD (shouldCache = true on p. 1224).
  • During the first count() The first job computes the partitions and, since they are marked, keeps them; the second job reads them.
  • During the second count() By then they are already in memory: the second job is the one that profits.
  • Never: cache() works only on DataFrames RDDs are "cacheable" (p. 1208); the job example caches an RDD.

Which of these transformations need a shuffle, and so start a new stage? Select all that apply.

  • groupByKey() The values of one key lie in many partitions and must be gathered.
  • map() Each partition is transformed where it lies.
  • reduceByKey() Wide: it combines inside each partition first, but the partial sums of a key still have to meet.
  • union() The result just lists the partitions of both inputs: narrow.
  • distinct() Equal records from different partitions must meet: wide, as p. 1214 lists it.
  • coalesce(2) Whole partitions are merged and none is split: narrow, without a shuffle.

The test: must some input partition be split and sent to several output partitions?

Why must the functions passed to Spark transformations be deterministic?

  • Because Spark rebuilds lost partitions by running them again, and must get the same data back Recovery by lineage assumes that the recipe gives the same result twice.
  • Because Python lambdas cannot draw random numbers They can; that is exactly what would make recovery rebuild different data.
  • Because every action runs each function twice to check the result No such check exists; a function runs again only when a partition is recomputed.
  • Because the DAGScheduler sorts operations by their output The DAGScheduler cuts stages at shuffles; it never looks at the values.

The worker holding partition 2 of the cached RDD messages dies. What does the next action recompute?

  • Only partition 2: its block is re-read from HDFS and filter and map are applied again Lineage rebuilds what was lost; the other cached partitions are read from memory.
  • All partitions of messages, because the cache is no longer complete Recovery is per partition: the cached ones are still valid.
  • Nothing: Spark keeps a replica of every cached partition The default cache keeps one copy; Spark relies on lineage, not on replicas.
  • Nothing: the driver keeps a copy of the data The driver keeps the lineage and the results of actions, not the data of the partitions.

Which component splits a job into stages?

  • The DAGScheduler "split graph into stages of tasks — submit each stage as ready" (p. 1225).
  • The TaskScheduler It launches the tasks of a stage through the cluster manager and retries failed or straggling ones.
  • The cluster manager It hands out machines or executors; it knows nothing of RDDs.
  • The block manager It stores and serves blocks on each worker.

On the chart of p. 1218, an aggregation written with DataFrames takes about 2 s in Python and in Scala, while with RDDs Python needs about 9.5 s and Scala about 4.2 s. Why?

  • The DataFrame API only describes the computation, and Spark runs the optimized plan in the JVM; RDD lambdas written in Python run in Python processes, record by record The language writes the plan; the JVM executes it. Only RDD code ships every record to Python.
  • Python DataFrames are translated into Scala source code by the programmer Nobody translates anything; the declarative API is the reason.
  • DataFrames are always cached, RDDs never Neither is cached unless you ask for it.
  • The two charts use different amounts of data It is one chart and one task: 10 million integer pairs.

Key take-aways

What to watch and read