What is a shuffle and why does everyone keep saying the word?
A shuffle repartitions data across the cluster so rows with the same key land together: every wide operation (groupBy, join, distinct, repartition) pays for one. It is the unit of cost in Spark: disk, network, and serialization all spike at shuffle boundaries, which is why plans are read shuffle-first.
reduceByKey versus groupByKey?
reduceByKey combines map-side before shuffling, so the wire carries partial aggregates; groupByKey ships every raw value and materializes them per key on one executor. Same output for associative aggregations, wildly different memory profile. In the DataFrame API, agg() gets the combiner behavior automatically.
repartition versus coalesce?
repartition shuffles to exactly N partitions, up or down, evenly. coalesce merges existing partitions without a shuffle, down only, and can leave them uneven. Writing fewer output files wants coalesce; rebalancing skew before an expensive stage wants repartition, and paying the shuffle there is the point.
When does a broadcast join beat sort-merge?
When one side fits comfortably in each executor's memory, typically under a few hundred MB after compression. The small side ships to every executor once and the big side never shuffles. The failure mode is broadcasting something that grew: yesterday's 100 MB dimension is next quarter's 4 GB OOM.
What is key skew and how do you spot it?
One key holding a disproportionate share of rows, so one partition and one task do most of the work. The tell is a stage where the max task duration dwarfs the median while shuffle-read is similarly lopsided. Fixes in escalating order: filter the degenerate key, broadcast the other side, salt the hot keys.
What does AQE change about all of this?
Adaptive Query Execution replans at runtime using real shuffle statistics: it coalesces small partitions, flips sort-merge to broadcast when a side turns out small, and splits skewed partitions. It does not remove the need to understand the plan; it removes the need to hand-tune the cases it can see.
Why are Python UDFs slow, and what replaces them?
Each row round-trips between the JVM and a Python worker with serialization both ways, and the optimizer treats the UDF as a black box, killing pushdown. Native column functions stay in the JVM and stay optimizable; pandas UDFs amortize the boundary with Arrow batches when native ops cannot express the logic.
What makes Structured Streaming exactly-once?
3 parts together: replayable sources (Kafka offsets in the checkpoint), deterministic state bounded by a watermark, and idempotent or transactional sinks (Delta). Any one alone is not enough, which is what makes duplicate counts such a common production failure.
What is data skew in Spark and how do you fix it?
Skew is one join or group key holding a disproportionate share of rows, so a single task processes most of the data while the rest of the cluster idles and the stage hangs at 99%. Spot it in the Spark UI as a task whose shuffle-read bytes dwarf the median. Fixes in order of preference: broadcast the small side to remove the shuffle, enable AQE skew join handling, or salt the hot key with a random suffix, join on the salted key, then re-aggregate.
What is predicate pushdown and partition pruning?
Both cut data before it is read. Predicate pushdown sends the filter down into the file scan, so Parquet row groups whose min/max statistics cannot match are skipped. Partition pruning goes further and skips whole directories when the filter is on the partition column. Both are defeated by wrapping the column in a function or by a filter the optimizer cannot see through, such as one hidden inside a UDF.
How does the Catalyst optimizer work?
Catalyst rewrites the query through phases: parse to an unresolved logical plan, resolve against the catalog, optimize with rule-based transformations (predicate pushdown, constant folding, column pruning), then generate physical plans and pick one by cost. The practical consequence is that Python UDFs are opaque boxes it cannot rewrite or push down, which is why replacing one with a built-in expression often changes the whole plan.
What is the difference between a wide and a narrow transformation?
A narrow transformation (map, filter, union) needs only the partition it is given, so it pipelines within a stage with no data movement. A wide transformation (groupBy, join, distinct, repartition) needs rows from other partitions, which forces a shuffle and a stage boundary. Counting stage boundaries in a plan tells you how many shuffles a job pays for.
What is the difference between cache and persist, and when do you use either?
cache() is persist() with the default MEMORY_AND_DISK storage level; persist() lets you name a different level. Use them when a DataFrame is consumed more than once and recomputing it means re-reading the source or redoing a shuffle. Caching something read once wastes memory and can evict work that mattered. Unpersist when the reuse window closes.
How does Spark memory management work and why do executors OOM?
Executor memory splits into an execution region (shuffles, joins, sorts) and a storage region (cached blocks), which borrow from each other, plus user memory and a fixed overhead. OOM usually means one of: a skewed partition too large for one task, a collect() pulling the result set to the driver, an oversized broadcast, or too little overhead for Python worker processes in PySpark. The fix follows the cause; raising memory blindly hides skew rather than solving it.
How do you read the Spark UI to find a slow stage?
Start at the Stages tab and sort by duration. Open the slowest stage and compare the task duration and shuffle-read distribution: a max far above the median is skew, uniformly slow tasks with heavy spill is memory pressure, and a huge task count on a small input is over-partitioning. The SQL tab then maps that stage back to the plan node so you know which operator to change.
What is the difference between DataFrame, Dataset, and RDD?
RDD is the low-level distributed collection with no schema, so Catalyst cannot optimize it. DataFrame is a Dataset of Row with a schema, fully optimized, and the right default. Dataset adds compile-time typing but exists only in Scala and Java. In PySpark the choice is effectively DataFrame, and dropping to RDD is a deliberate decision that gives up the optimizer.
What is a checkpoint and how does it differ from caching?
Caching keeps a computed result available for reuse but preserves the lineage, so a lost partition is recomputed. Checkpointing writes the data to reliable storage and truncates the lineage entirely. Long iterative jobs checkpoint to stop the lineage graph from growing without bound; streaming jobs checkpoint to store offsets and state so a restart resumes exactly where it stopped.
What is the small files problem and how do you avoid it?
Thousands of tiny output files make every downstream read pay per-file listing and open overhead, and they bloat the metastore. It usually comes from over-partitioning or from a streaming sink writing per micro-batch. Fixes: coalesce before writing, partition on a lower-cardinality column, or run a compaction job. On Delta or Iceberg, OPTIMIZE or a rewrite action does the compaction for you.