Interview prep

PySpark interview questions with real answers

Interviewers don't want the docs recited back. They want to know whether you've debugged a slow job at 2am. These are the questions that separate the two — with the code.

1. Explain lazy evaluation and why it matters

df = spark.read.parquet("s3://events/")   # nothing runs
df = df.filter(col("country") == "BR")     # nothing runs
df = df.groupBy("user_id").count()         # nothing runs
df.write.parquet("s3://out/")              # ACTION -> the whole plan executes

The payoff: Catalyst sees the entire plan before executing, so it can push the filter down into the Parquet scan and prune columns and partitions. The trap: errors surface at the action, far from the line that caused them.

2. Narrow vs wide transformations

Narrow (select, filter, withColumn): each output partition depends on one input partition — no network. Wide (groupBy, join, distinct, repartition): rows must be redistributed by key, which triggers a shuffle and creates a new stage. Stage boundaries in the Spark UI are exactly the shuffle points.

3. How do you diagnose and fix data skew?

Diagnose: in the Spark UI stage view, look at task duration — median 2s, max 400s means skew.

# Option A — let AQE handle it (Spark 3+)
spark.conf.set("spark.sql.adaptive.enabled", True)
spark.conf.set("spark.sql.adaptive.skewJoin.enabled", True)

# Option B — broadcast the small side
from pyspark.sql.functions import broadcast
orders.join(broadcast(countries), "country_code")

# Option C — salt the hot key
from pyspark.sql.functions import col, lit, rand, floor, concat_ws
SALT = 16
big   = big.withColumn("k", concat_ws("_", col("user_id"), floor(rand()*SALT)))
small = (small
    .withColumn("s", explode(array([lit(i) for i in range(SALT)])))
    .withColumn("k", concat_ws("_", col("user_id"), col("s"))))
joined = big.join(small, "k").drop("k", "s")

4. When does a broadcast join break?

When the "small" side isn't small. The driver collects it and ships it to every executor, so a 2 GB broadcast with 200 executors is a driver OOM waiting to happen. Default threshold is spark.sql.autoBroadcastJoinThreshold = 10 MB. Raise it deliberately, and remember that broadcast size is the in-memory size, not the compressed Parquet size.

5. repartition vs coalesce

df.repartition(200, "customer_id")  # full shuffle, even distribution, can increase
df.coalesce(10)                     # no shuffle, merges partitions, can only decrease

Use coalesce right before writing to avoid thousands of tiny files. Use repartition when partitions are unbalanced or you need to co-locate a join key. coalesce(1) on a big DataFrame funnels everything into one task — a classic accidental bottleneck.

6. Why avoid Python UDFs?

# Slow: row-by-row serialization JVM <-> Python, opaque to Catalyst
@udf("double")
def to_usd(cents): return cents / 100.0

# Fast: native expression, optimizable, no serialization
df.withColumn("usd", col("amount_cents") / 100.0)

# Middle ground when you truly need Python: Arrow-based pandas UDF
@pandas_udf("double")
def to_usd(s: pd.Series) -> pd.Series: return s / 100.0

7. Write a window function in PySpark

The classic: latest record per key (deduplication).

from pyspark.sql import Window
from pyspark.sql.functions import row_number, col

w = Window.partitionBy("order_id").orderBy(col("ingested_at").desc())

latest = (df
    .withColumn("rn", row_number().over(w))
    .filter(col("rn") == 1)
    .drop("rn"))

Follow-up they will ask: row_number vs rank vs dense_rank, and why a window without partitionBy forces all data into a single partition.

8. Common OOM causes and fixes

  • collect() on a large DataFrame — driver OOM. Use take(n) or write to storage.
  • Too-large broadcast — lower the threshold or disable auto-broadcast for that query.
  • Skewed partition — one executor holds a giant key group; salt or enable AQE.
  • Over-caching — cache only reused DataFrames and unpersist() after.
  • Too few partitions for the data volume — aim for ~128 MB per partition.

9. The behavioural one you'll definitely get

"Tell me about a Spark job you made faster." Structure the answer as: symptom → what the Spark UI showed → hypothesis → change → measured result. "The daily job took 4h; the UI showed one stage at 3h20 with max task 60x the median; the join key was 40% nulls; I filtered nulls before the join and enabled AQE skew handling; runtime dropped to 35 minutes." That single sentence pattern passes most interviews.

Practice, don't memorize

These questions are only answerable convincingly if you've broken and fixed real jobs. The DataForge PySpark course and daily Bug Hunt put you in front of broken Spark code every day, and the full interview guide covers SQL, modeling and system design.

FAQ

What PySpark questions are asked in data engineering interviews?
Expect four buckets: execution model (lazy evaluation, transformations vs actions, jobs/stages/tasks), performance (shuffles, partitioning, skew, broadcast joins, caching), API fluency (DataFrame ops, window functions, UDF trade-offs), and debugging (reading the Spark UI, fixing OOM and skew).
What is the difference between a transformation and an action in Spark?
Transformations (select, filter, join, groupBy) are lazy — they only build the logical plan. Actions (collect, count, write, show) trigger execution of that plan. Nothing runs until an action is called, which is why a broken transformation can appear to 'work' until the write step.
What causes a shuffle in Spark?
Any operation that needs rows with the same key on the same executor: groupBy, join (non-broadcast), distinct, repartition, and window functions with a partitionBy. Shuffles write to disk and cross the network, so they dominate runtime in most slow jobs.
How do you handle data skew in PySpark?
Enable Adaptive Query Execution with skew join handling, broadcast the small side when it fits, or salt the hot key by appending a random suffix, aggregating, then removing the salt. Diagnose it first in the Spark UI: one task taking 20x longer than the median is skew.
Is PySpark slower than Scala Spark?
For DataFrame and SQL operations, no — both compile to the same Catalyst plan and run in the JVM. PySpark is slower only when you use Python UDFs, which serialize rows between JVM and Python. Prefer built-in functions or pandas UDFs (Arrow-based).
cache() or persist() — which should I use?
cache() is persist(MEMORY_AND_DISK). Use it when a DataFrame is reused by two or more actions, and unpersist when done. Caching something used once just wastes memory and can trigger spills.

Ready to start?

7 days free. Then less than a coffee per month.

Train on real Spark code — 7 days free