Core Architecture
Q1: What is the difference between an RDD, a DataFrame, and a Dataset?
- RDD (Resilient Distributed Dataset) β low-level, type-safe, no optimizer. Fine-grained control. Avoid in modern Spark unless you need custom transformations.
- DataFrame β distributed table with named columns, no compile-time type safety. Runs through Catalyst optimizer. Use this by default.
- Dataset β typed DataFrame. Compile-time type safety. JVM only (Java/Scala).
Interview tip: In an interview, say "I use DataFrames/Spark SQL because Catalyst and Tungsten optimize them automatically. RDDs are useful when you need to control the exact partitioning or serialize custom objects."
Q2: Explain the Spark DAG execution model.
When you call an action (.count(), .write()), Spark builds a DAG (Directed Acyclic Graph) of transformations. The DAG is submitted to the DAG Scheduler, which splits it into stages at shuffle boundaries.
- Transformations are lazy β no execution until an action is called
- Wide transformations (groupBy, join, repartition) cause shuffles β new stage
- Narrow transformations (filter, map, select) stay in the same stage
Each stage is split into tasks β one task per partition. Tasks are sent to executors by the Task Scheduler.
Q3: What is the difference between a transformation and an action?
- Transformations β lazy operations that return a new RDD/DataFrame. They are not executed until an action is called. Examples:
filter,map,join,groupBy,select. - Actions β trigger computation and return a result to the driver or write to storage. Examples:
count,collect,show,write,take,reduce.
Common mistake: Calling .collect() on a large DataFrame β this brings all data to the driver. Use .write() or aggregations instead.
Shuffles and Partitioning
Q4: What is a shuffle? When does it happen?
A shuffle is Spark redistributing data across partitions β data is written to disk on executors, sorted, and re-read by other executors. It's the most expensive operation in Spark.
Shuffles happen on:
groupBy/reduceByKeyjoin(unless broadcast join)repartition/coalescedistinctsortByon a distributed column
To reduce shuffles:
- Use
broadcast()for small tables in joins (avoids shuffle) - Partition your data on the join key before joining
- Use
reduceByKeyinstead ofgroupByKey(partial aggregation before shuffle)
Q5: How many partitions should a Spark job have?
Rule of thumb: 2-4 partitions per CPU core in the cluster.
For a cluster with 20 executors Γ 5 cores = 100 cores β 200-400 partitions is reasonable.
Default spark.sql.shuffle.partitions = 200 (set this based on your data size):
- Too few β each partition is large β memory pressure, OOM
- Too many β too many small files β metadata overhead, slow writes
Practical formula: aim for 100-200 MB per partition after shuffle.
# Check partition count
df.rdd.getNumPartitions()
# Repartition
df = df.repartition(400, "user_id") # hash partition on user_id
Adaptive Query Execution (AQE)
Q6: What is AQE and what problems does it solve?
AQE (Adaptive Query Execution, Spark 3.0+) dynamically changes the query plan at runtime based on actual data statistics rather than estimates.
It solves three problems:
- Skewed joins β automatically splits skewed partitions into smaller ones
- Dynamic partition coalescing β merges tiny shuffle partitions after a shuffle (reduces the 200 empty partitions problem)
- Join strategy switching β can switch from sort-merge join to broadcast join if a table turns out to be small
Enable it: spark.sql.adaptive.enabled = true (default in Spark 3.2+)
Skew Handling
Q7: What is data skew and how do you handle it?
Skew occurs when some partitions have far more data than others β typically caused by null keys, popular categories, or uneven distributions. One task takes 10x longer than others, blocking the stage.
Detection: Check the Spark UI β Stage Details β Task Time column. If one task is >>mean, you have skew.
Solutions:
- Broadcast join β if the small table fits in memory, broadcast it to avoid the shuffle entirely
- Salting β add a random salt to the skewed key, join, then aggregate:
from pyspark.sql.functions import concat, lit, floor, rand SALT = 10 large = df1.withColumn("salt", (rand() * SALT).cast("int")) large = large.withColumn("key_salted", concat("key", lit("_"), "salt")) small = df2 small = small.withColumn("salt", explode(array([lit(i) for i in range(SALT)]))) small = small.withColumn("key_salted", concat("key", lit("_"), "salt")) result = large.join(small, "key_salted").drop("key_salted", "salt") - AQE skew join β enable
spark.sql.adaptive.skewJoin.enabled = truefor automatic handling - Repartition before join on a more evenly distributed column
Memory Tuning
Q8: Explain Spark's memory model.
Spark uses the JVM heap divided into regions:
- Execution memory β shuffles, sorts, aggregations (uses off-heap by default in Tungsten)
- Storage memory β cached DataFrames/RDDs
- User memory β your custom data structures
spark.memory.fraction (default 0.6) β the fraction of JVM heap for execution + storage. The remaining 40% is user + overhead.
spark.memory.storageFraction (default 0.5) β within the managed region, this fraction is for storage. Storage can borrow from execution and vice versa.
Tuning tips:
- OOM on executor β increase
spark.executor.memoryor reducespark.executor.cores - GC overhead too high β increase off-heap memory:
spark.memory.offHeap.enabled = true - Caching too much β use
MEMORY_AND_DISKinstead ofMEMORY_ONLY
Joins
Q9: What are the join strategies in Spark and when does each apply?
| Strategy | When Used | Pros | Cons |
|---|---|---|---|
| Broadcast Hash Join | Small table fits in memory (< spark.sql.autoBroadcastJoinThreshold, default 10MB) | No shuffle | OOM if threshold too high |
| Sort-Merge Join | Both tables are large, sorted on join key | Works at any scale | Expensive shuffle |
| Shuffle Hash Join | One side is significantly smaller | Faster than SMJ | OOM risk if bucket too large |
Force a broadcast join with a hint:
from pyspark.sql.functions import broadcast
result = large_df.join(broadcast(small_df), "user_id")
Structured Streaming
Q10: What are the output modes in Spark Structured Streaming?
- Append β only new rows are written. No updates to existing rows. Requires watermark for stateful ops.
- Update β only rows that were updated since last trigger are written. Works with aggregations.
- Complete β the entire result table is re-written every trigger. Only valid for aggregations.
Q11: What is a watermark in Structured Streaming?
A watermark tells Spark how late data can arrive before it's dropped. Without a watermark, Spark must keep all state forever.
df.withWatermark("event_time", "10 minutes") \
.groupBy(window("event_time", "5 minutes"), "user_id") \
.count()
This tells Spark: drop any event that arrives more than 10 minutes after the event_time. State for a window is cleaned up 10 minutes after the window closes.
Delta Lake
Q12: What is Delta Lake and what problems does it solve?
Delta Lake adds ACID transactions on top of Parquet files stored in object storage (S3/GCS/ADLS). It solves:
- Concurrent writes β multiple jobs can write without corrupting data
- Time travel β
SELECT * FROM delta.events VERSION AS OF 5orTIMESTAMP AS OF - Schema evolution β add/rename columns without rewriting all data
- Upserts (MERGE) β efficient CDC ingestion without full rewrites
- Compaction β
OPTIMIZEmerges small files;ZORDERclusters related data
# Upsert (MERGE)
from delta.tables import DeltaTable
dt = DeltaTable.forPath(spark, "/delta/users")
dt.alias("target").merge(
updates.alias("source"),
"target.user_id = source.user_id"
).whenMatchedUpdateAll().whenNotMatchedInsertAll().execute()
Common Interview Questions
Q13: Why would you choose Spark over MapReduce?
- Spark is 100x faster for iterative workloads because it caches intermediate data in memory vs writing to HDFS after every step
- Spark supports streaming, ML, and graph processing in one API
- DAG optimization with Catalyst is more efficient than chained MapReduce jobs
Q14: What is the difference between coalesce and repartition?
repartition(n)β full shuffle, can increase or decrease partitions, evenly distributedcoalesce(n)β no shuffle, can only reduce partitions, may be uneven, faster
Use coalesce before writing to reduce output files. Use repartition when you need evenly-sized partitions (before a join or aggregation).