Data Skew and Partitioning
The problem of the heavy partition
Skew is the silent killer of parallel jobs: a few hot keys or partitions do all the work while the cluster idles. Partitioning done right (by the natural query dimension) prevents skew, and salting/AQE fix it when it appears.
What you will learn
- Recognize skew from timing and partition-size data
- Prevent skew with deliberate partitioning and bucketing
- Fix hot keys with salting and adaptive execution
What is this?
Data skew means some partitions (or keys) hold far more data than others, so a few tasks run an order of magnitude longer and the cluster idles waiting for them. Distribution (repartition, bucketing) chooses how data is split; skew is what happens when that distribution is uneven — hot users, null keys, or a dominating join value.
Why do you need it?
- Skew wastes the majority of your cluster for the duration of one straggler.
- Shuffle joins amplify skew: the hot key's reducer waits for gigabytes others never see.
- Fixed by engineering, not hardware: salting, range partitioning, or AQE.
- Scheduling a skew fix is cheaper than the repeated cost of wasted compute.
Mental model
Ferry boats crossing a river
Partitioning and skew
Thirty identical boats leave at once, but one dock sends 90% of the cars — so twenty-nine boats idle while the big wait. You'd rebalance the loads across all docks (partition) or split the heavy group (salt).
How it works
- Partitions are the units of parallelism: one task per partition per stage.
- Hash partitioning routes each key to one partition, reliable but skew-prone (a hot key → one partition).
- Range partitioning sorts keys into contiguous ranges, balancing by value spread.
- Bucketing pre-partitions on a column so later joins/aggregations can skip shuffles entirely.
- Remedies: salt hot keys (append a random suffix to split load), plus AQE's skewJoin splitting.
Skew vs balanced partitioning.
In production
- A null customer key swallowing a join is split by salting so no single reducer babysits the NULLs.
- Range partitioning on order_date keeps daily range scans balanced and avoids month-boundary hotspots.
- Bucketing fact tables on customer_id makes customer joins probe-free (collocated).
Best practices
- Pick the partition dimension from your most common filter/join — usually time or tenant.
- Keep an eye out for NULL/empty keys — they're the classic silent hot key.
- Bucketed tables (on the join key) reduce shuffle data dramatically for repeated joins.
- Let AQE split skewed joins; reserve manual salting for the worst offenders.
Common mistakes
- Partitioning on a high-cardinality-but-skewed column (like user_id of one app's power user).
- Salting the wrong key, wrecking the second-hop aggregation semantics.
- Ignoring a single massive partition and 'fixing' the cluster instead.
- Bucketing with too many buckets so each bucket is tiny — a new kind of skew.
Common issues
One join task alone eats 30 minutesA hot join key or NULL values concentrated into one partition
Salt the key (split), split the NULLs with another dimension, or run the hot-key rows through a separate path.
AQE not splitting a skewed joinConfig disabled or the skew threshold not met
Enable spark.sql.adaptive.skewJoin.enabled and tune skewJoin.skewedPartitionFactor/threshold.
Bucketed table not avoiding a shuffleBucket count/type mismatch between fact and dimension
Match the bucket number and bucket column exactly across joined tables.
Debugging tips
- Look at max vs median task duration in the UI — 10x gap means skew.
- Check per-partition byte counts with sizeInBytes before joins.
- MySQL NULL hot keys show up as a single dominant reduce task.
- Compare logical vs physical plan to see whether AQE actually split the skew.
Interview checkpoint
Hands-on lab
- Build a dataset with an extreme hot key
- Demonstrate the imbalance by per-partition sizes
- Rebalance with a salted second grouping and compare
Prove Skew and Balance It with DuckDB
30 min · advanced
Quick quiz
The classic silent hot key in production data is...
Range partitioning is symmetrical with hash partitioning for all workloads.