withsoon
Home/Interview/Spark Interview Questions β€” Internals, Optimization, and Production Patterns
Interviewadvanced7 min read

Spark Interview Questions β€” Internals, Optimization, and Production Patterns

The most-asked Apache Spark interview questions: RDDs vs DataFrames, DAG execution, shuffle, AQE, skew handling, memory tuning, and Structured Streaming.

by Prasoon ParasharπŸ“… 2026-06-16
#spark#interview#big-data#pyspark#streaming

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 / reduceByKey
  • join (unless broadcast join)
  • repartition / coalesce
  • distinct
  • sortBy on 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 reduceByKey instead of groupByKey (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:

  1. Skewed joins β€” automatically splits skewed partitions into smaller ones
  2. Dynamic partition coalescing β€” merges tiny shuffle partitions after a shuffle (reduces the 200 empty partitions problem)
  3. 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:

  1. Broadcast join β€” if the small table fits in memory, broadcast it to avoid the shuffle entirely
  2. 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")
    
  3. AQE skew join β€” enable spark.sql.adaptive.skewJoin.enabled = true for automatic handling
  4. 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.memory or reduce spark.executor.cores
  • GC overhead too high β†’ increase off-heap memory: spark.memory.offHeap.enabled = true
  • Caching too much β†’ use MEMORY_AND_DISK instead of MEMORY_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:

  1. Concurrent writes β€” multiple jobs can write without corrupting data
  2. Time travel β€” SELECT * FROM delta.events VERSION AS OF 5 or TIMESTAMP AS OF
  3. Schema evolution β€” add/rename columns without rewriting all data
  4. Upserts (MERGE) β€” efficient CDC ingestion without full rewrites
  5. Compaction β€” OPTIMIZE merges small files; ZORDER clusters 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 distributed
  • coalesce(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).

Get new guides when they land

System design, interview Q&A, and cheatsheets β€” no spam.

No spam. Unsubscribe any time.