4.10 Data skew & salting¶
Concept¶
A shuffle spreads rows across partitions by a key. If one key has far more rows than the others, its partition becomes huge — and since a shuffle stage is a barrier, the whole job waits on that one slow task. That's data skew, and it's the most common reason a Spark job "runs for hours on the last 2 tasks."
Even: Skewed:
[P1][P2][P3][P4] all ~equal [P1][P2][P3][ P4 ..................... ] ← one giant partition
→ finish together → 3 tasks done in minutes, 1 runs for hours (straggler)
Detecting it¶
In the data flow monitor (or Spark UI): - One partition has far more rows / much longer task time than the rest. - The job flies to ~95%, then stalls on 1–2 tasks.
The usual causes¶
| Cause | Example |
|---|---|
| Null / blank keys (most common!) | thousands of rows with customer_id = null all hash to one partition |
| Hot keys | one product_id = "UNKNOWN" holds 40% of the rows |
| Low-cardinality key | grouping by a column with only a few distinct values |
Fixes, in order of preference¶
1. Filter/handle null & garbage keys before the shuffle (easiest, biggest win)¶
Nulls all collapse into one monster partition. Drop or quarantine them first.
For a dedup on a near-unique key, this is usually the whole fix — the skew was the nulls.2. Broadcast the small side of a join (skew-proof for joins)¶
If one side is small, broadcasting it means no shuffle → no skew possible on the big side. (See 4.9 — Join strategies.)
3. More partitions / round-robin (mild skew)¶
Rebalances moderately uneven data — not a single extreme hot key.
4. Salting (for a genuine hot key)¶
Split the hot key across many partitions with a random salt, aggregate in two stages, then combine.
Salting an aggregation — sum(amount) by product_id, where "UNKNOWN" dominates:
N = 16
# Stage 1: add salt → the hot key now spreads across N partitions
salted = df.withColumn("salt", (rand() * N).cast("int"))
partial = salted.groupBy("product_id", "salt").agg(sum("amount")) # partial sums, parallel
# Stage 2: drop the salt, combine the partials
final = partial.groupBy("product_id").agg(sum(partial_sum)) # correct total
UNKNOWN is now computed by N tasks in parallel instead of one straggler.
ADF: Derived Column salt = round(rand()*16) → Aggregate groupBy(key, salt) →
second Aggregate groupBy(key).
Salting a join — salt the skewed (big) side randomly, and fan out the small side (replicate each row once per salt value) so matches still happen:
orders_s = orders.withColumn("salt", (rand()*N).cast("int")) # random salt on big side
products_x = products.crossJoin(salts(0..N-1)) # each product × every salt
result = orders_s.join(products_x, ["product_id", "salt"]) # join on composite key
5. Pre-aggregate before the shuffle¶
groupBy already does a partial (map-side) aggregate before the shuffle, so far less data
crosses the network — one reason group-by tolerates skew better than a Window does. Always
filter and pre-aggregate early.
Decision guide¶
Skew from null/blank keys? → filter/quarantine them first (fix 1)
Joining a big table to a small? → broadcast the small side (fix 2)
Mild general imbalance? → round-robin / more partitions (fix 3)
One legitimately huge key? → salting + two-stage aggregate/join (fix 4)
Always: → filter & pre-aggregate before shuffle(fix 5)
Key terms, at a glance¶
| Term | Plain meaning |
|---|---|
| Data skew | uneven partition sizes → one task does most of the work |
| Straggler | the slow task the whole stage waits on |
| Salt | a random 0..N-1 tag that splits a hot key across partitions |
| Two-stage aggregation | partial aggregate by (key, salt), then final by key |
| Fan-out | replicating the small join side once per salt so salted keys still match |
You can now…¶
- Recognize skew (straggler tasks) and name its usual cause (null/hot keys)
- Pick the right fix: filter nulls, broadcast, repartition, or salt
- Explain salting for both an aggregation and a skewed join, in two stages
- Say why filtering/pre-aggregating before the shuffle always helps
🎯 This runs unchanged on Azure, Databricks, Snowflake & Fabric¶
Skew, salting, and partitioning are Apache Spark concepts — the same techniques apply in Databricks, Synapse/Fabric Spark, and any Spark-backed engine; only the UI differs.