🔍 Ctrl+K
🟢 Easy

Narrow Transformations

A narrow transformation does not require data to be shuffled between partitions. Each input partition contributes to at most one output partition.

Common narrow transformations include:

Python
# Narrow transformations — each partition maps to one output partition
map()          # Apply function to each element
filter()       # Keep elements matching condition
flatMap()      # Map + flatten
mapPartitions()# Apply function per partition
union()        # Combine two RDDs
Partition-Level Operation — Click a component for details
Partition 1
map()
Partition 1'
Partition 2
map()
Partition 2'
Partition 3
filter()
Partition 3'
Input Partition 1

Contains a subset of the total dataset. Each partition is processed independently by a single task on an executor.

Input Partition 2

Another independent slice of data. No dependency on Partition 1 or 3 — true parallelism.

Input Partition 3

Each partition can even apply a different narrow transformation — Spark handles this transparently.

map() — Narrow Transformation

Each input element produces exactly one output element. The function runs locally on the executor — no data movement across the network.

filter() — Narrow Transformation

Evaluates a predicate on each element. Elements that pass are kept; others are discarded. Still one-to-one — no shuffle.

Output Partition 1'

Result of applying map() to Partition 1. Same partition index, same executor — zero network I/O.

Output Partition 2'

Processed independently. If this transformation were pipelined with another narrow op, both would run in the same task.

Output Partition 3'

May have fewer elements than the input (filter removed some), but the partition boundary is preserved.

Key Characteristics

  • One-to-one partition relationship
  • No data movement between partitions
  • Usually faster execution
  • Can be pipelined together (fused into a single task)
Interview Tip
Narrow transformations are efficient because they don't require data movement across the network. Spark can pipeline multiple narrow transformations into a single stage, avoiding costly shuffle operations.
🔴 Advanced

Wide Transformations

A wide transformation requires data to move between partitions (shuffle). Each input partition can contribute to multiple output partitions.

Common wide transformations include:

Python
# Wide transformations — data shuffle required
groupByKey()    # Group values by key
reduceByKey()   # Aggregate values by key
join()          # Join two RDDs/DataFrames
sortByKey()     # Sort by key
distinct()      # Remove duplicates
repartition()   # Change number of partitions
coalesce()      # Reduce partitions (no full shuffle)
Shuffle Operation — Click a component for details
Partition 1
Partition 2
Partition 3
⚡ SHUFFLE
New Partition A
New Partition B
New Partition C
Input Partition 1

Contains key-value pairs. During shuffle, data is re-partitioned by key — records with the same key must end up on the same output partition.

Input Partition 2

Data from this partition will be sent to multiple output partitions based on the partitioning key. This cross-partition data movement is the shuffle.

Input Partition 3

Same as above — records are hashed by key to determine their target partition. Network I/O is proportional to data volume.

⚡ The Shuffle

The most expensive operation in Spark. Data is serialized, written to disk, transferred across the network, and deserialized. This creates a stage boundary in the DAG.

New Partition A

Contains all records (from all input partitions) whose key hashes to partition A. The number of output partitions is determined by spark.sql.shuffle.partitions (default 200).

New Partition B

After shuffle, each output partition is processed independently again. Wide transformations effectively "reset" the pipeline.

New Partition C

The new partitions may be on completely different executors than the original ones — this is why shuffle is expensive.

Key Characteristics

  • Many-to-many partition relationship
  • Requires data shuffle across network
  • Usually slower execution
  • Creates new stage boundaries in the DAG
Interview Tip
Wide transformations cause shuffle and can create a new stage. Minimize them for better performance. When possible, use reduceByKey instead of groupByKey to reduce the amount of data shuffled.
🟡 Intermediate

Narrow vs Wide — Comparison

Feature Narrow Wide
Shuffle Required No Yes
Data Movement Low High
Performance Usually faster Usually slower
Relationship One-to-one Many-to-many
Stage Boundary No Yes
Examples map, filter, union groupBy, join, reduceByKey
Network I/O Minimal Significant
Pipelining Yes — fused into one task No — creates stage boundary
Narrow — One-to-One
P1
map()
P1'
P2
map()
P2'
Wide — Many-to-Many
P1
P2
Shuffle
A
B
🟡 Intermediate

Common Transformations

map() Narrow

Applies a function to each element in the RDD and returns a new RDD.

rdd = sc.parallelize([1, 2, 3, 4, 5])
mapped = rdd.map(lambda x: x * 2)
print(mapped.collect())  # [2, 4, 6, 8, 10]
filter() Narrow

Returns a new RDD containing only the elements that satisfy a given condition.

rdd = sc.parallelize([1, 2, 3, 4, 5])
filtered = rdd.filter(lambda x: x > 2)
print(filtered.collect())  # [3, 4, 5]
flatMap() Narrow

Applies a function that returns an iterator for each element, then flattens the results into a single RDD.

rdd = sc.parallelize(["hello world", "hi spark"])
flat = rdd.flatMap(lambda x: x.split(" "))
print(flat.collect())
# ["hello", "world", "hi", "spark"]
reduceByKey() Wide

Aggregates values for each key using a reduce function. More efficient than groupByKey() because it combines data locally before shuffle.

rdd = sc.parallelize([("a", 1), ("b", 2), ("a", 3)])
reduced = rdd.reduceByKey(lambda a, b: a + b)
print(reduced.collect())
# [("a", 4), ("b", 2)]
groupByKey() Wide

Groups all values for each key into an iterable. Generally less efficient than reduceByKey() because it shuffles all data before aggregation.

rdd = sc.parallelize([("a", 1), ("b", 2), ("a", 3)])
grouped = rdd.groupByKey()
for key, values in grouped.collect():
    print(f"{key}: {list(values)}")
# a: [1, 3]
# b: [2]
union() Narrow

Returns a new RDD containing all elements from both input RDDs. Does not deduplicate — use distinct() for that.

rdd1 = sc.parallelize([1, 2, 3])
rdd2 = sc.parallelize([3, 4, 5])
combined = rdd1.union(rdd2)
print(combined.collect())
# [1, 2, 3, 3, 4, 5]
🟢 Easy

Actions

Actions trigger Spark execution and return results to the driver or write data to storage. Without actions, no computation actually happens — Spark builds a DAG of lazy transformations.

Common Actions

collect() Action

Returns all elements of the RDD as a list to the driver.

rdd = sc.parallelize([1, 2, 3, 4, 5])
print(rdd.collect())  # [1, 2, 3, 4, 5]
⚠️ Avoid collect() on large datasets — it brings all data to the driver, which can cause out-of-memory errors.
count() Action

Returns the number of elements in the RDD.

rdd = sc.parallelize([1, 2, 3, 4, 5])
print(rdd.count())  # 5
show() Action

Displays the first 20 rows of a DataFrame in a tabular format. Useful for quick inspection.

df.show()
# +---+-----+
# | id|name |
# +---+-----+
# |  1|Alice|
# |  2|  Bob|
# +---+-----+
take() Action

Returns the first n elements of the RDD. Safer than collect() for large datasets.

rdd = sc.parallelize([1, 2, 3, 4, 5])
print(rdd.take(3))  # [1, 2, 3]
saveAsTextFile() Action

Writes the RDD elements to a text file. Each partition creates one output file.

rdd.saveAsTextFile("output/path")
# Creates output/part-00000, output/part-00001, ...
Interview Tip
Actions are the only way to trigger Spark execution. Transformations alone do nothing — they just build a logical plan. When an action is called, Spark optimizes the full DAG and executes it.
🔴 Advanced

Complete DAG Example

Let's trace a complete example showing how Spark builds a DAG from transformations and actions:

Python dag_example.py
# Step 1: Read data (narrow)
rdd = sc.textFile("data.txt")

# Step 2: Transform each line (narrow)
mapped = rdd.map(lambda x: (x.split(",")[0], x))

# Step 3: Filter (narrow)
filtered = mapped.filter(lambda x: x[0] == "key")

# Step 4: Group by key (wide — triggers shuffle!)
grouped = filtered.groupByKey()

# Step 5: Map over groups (narrow)
result = grouped.map(len)

# Step 6: Trigger execution (action)
result.collect()

Visual DAG — Stages & Shuffle Boundaries

Stage 0
├── textFile()
├── map()
└── filter()
⚡ SHUFFLE (groupByKey triggers exchange)
Stage 1
├── groupByKey()
└── map(len)
⚡ SHUFFLE (result materialized to driver)
Stage 2
└── collect()

How Spark Decides Stages

  1. Spark scans transformations from the action backwards
  2. Narrow transformations are pipelined into the same stage
  3. When a wide transformation is encountered, a new stage boundary is created
  4. Each stage runs tasks in parallel — one per partition
  5. Stages are connected by shuffle dependencies
Key Takeaway
Understanding the DAG helps you predict performance. Every shuffle is a potential bottleneck. By knowing which transformations are narrow vs. wide, you can write code that minimizes data movement and maximizes parallelism.