Back to Apache Spark: Core
Q49Question 49 of 72

Data skew in Spark: hot keys and how to detect it

Key Concepts
  • Data skew is uneven data distribution across partitions after a shuffle, caused by a hot key that appears disproportionately often.
  • One oversized partition creates a straggler task that delays the entire stage because the stage cannot finish until its slowest task completes.
  • Detect skew in the Spark UI Stages tab by comparing maximum task duration to the median – a ratio of ten to a hundred times indicates skew.
  • The straggler task also shows a much larger shuffle read than its peers and may spill to disk or fail with out-of-memory.
  • Profile the hot key by grouping on the join or group column and counting rows per key to surface dominant values.
  • Common banking hot keys include sweep accounts, suspense accounts, default placeholder ids, and null keys collecting all unmatched rows.
  • Distinguish skew from too few partitions: skew makes one task large while others are tiny, whereas too few partitions makes every task roughly equally large.

Definition

Data skew is one partition holding far more data than the rest due to a hot key, creating a straggler task – spot it in the Spark UI as one task taking ten to a hundred times longer with lopsided shuffle sizes.
  • Data skew – uneven distribution of data across partitions after a shuffle, where one or a few partitions hold far more records than the rest.
  • Hot key – a join or group key that appears disproportionately often, funneling most rows into a single partition.
  • Straggler task – the one task processing the oversized partition that runs much longer than all its peers, delaying the whole stage.
  • Detection – in the Spark UI Stages tab, the maximum task duration is ten to a hundred times the median, and the straggler's shuffle read is far larger than the others.
  • Root cause – any wide operation (join, groupBy, repartition) that hashes data by a skewed key creates the imbalance.

At the National Bank of India, imagine distributing the day's transactions to clerks by account number. Most accounts have a few transactions, but one corporate mega-account has millions. The clerk handling that mega-account is buried for hours while every other clerk finishes in minutes and sits idle. The whole batch is not done until that one overloaded clerk finishes. That one account is the hot key, and the imbalance is data skew.

Understanding the Concepts

What is data skew in Spark?

Data skew occurs when data is distributed unevenly across partitions after a shuffle. In Spark, wide operations like join, groupBy, and repartition hash rows by key so that all rows sharing the same key land in the same partition. If one key appears far more often than the others, its partition becomes enormous while the rest stay small. Since Spark runs exactly one task per partition, the oversized partition gets a single task that must process all those rows alone.

PYTHON
# Simulate skewed transaction data at the National Bank of India
from pyspark.sql import SparkSession
from pyspark.sql.functions import col, lit, rand

spark = SparkSession.builder.appName("SkewDemo").getOrCreate()

# 10 million rows for one corporate account, 1000 rows each for 999 others
skewed_txns = (
    spark.range(10000000).withColumn("account_id", lit("CORP_POOL_001"))
    .union(
        spark.range(999000).withColumn("account_id",
            (col("id") % 999 + 1).cast("string"))
    )
    .withColumn("amount", (rand() * 100000).cast("decimal(12,2)"))
)

# This groupBy triggers a shuffle; one partition gets 10M rows
account_totals = skewed_txns.groupBy("account_id").sum("amount")
account_totals.explain()

Why does data skew happen?

Skew arises from the nature of real-world data. In banking, certain accounts generate far more activity than others. A corporate pooling account at National Bank of India might handle millions of daily transactions while a personal savings account handles a handful. When Spark hashes the account id to decide which partition receives each row, the pooling account's rows all land in one partition.

Common sources of skew in banking pipelines include:

  • A corporate sweep or pooling account with millions of daily transactions.
  • A suspense account that collects all unmatched or error transactions.
  • A null or default placeholder id that every row without a real key carries.
  • A popular merchant id in a payments dataset that processes high volumes.
PYTHON
# Identify hot keys by counting rows per key
hot_keys = (
    skewed_txns.groupBy("account_id")
    .count()
    .orderBy(col("count").desc())
)
hot_keys.show(5)
TEXT
+-------------+--------+
|   account_id|   count|
+-------------+--------+
|CORP_POOL_001|10000000|
|          500|    1000|
|          250|    1000|
|          750|    1000|
|          125|    1000|
+-------------+--------+

How do you detect data skew in the Spark UI?

The Spark UI Stages tab is the primary detection tool. Open the stage that follows a shuffle and examine the task summary. With skew you will see:

  • The maximum task duration is ten to a hundred times the median task duration.
  • The maximum shuffle read size is far larger than the median shuffle read.
  • The event timeline shows most tasks finishing early and one task running alone for a long time.
  • The straggler task may show spill to disk because its partition does not fit in memory.
PYTHON
# After running account_totals.count(), open the Spark UI at localhost:4040
# Navigate to Stages tab -> click the groupBy stage
# Check the Summary Metrics table:
#   Metric          Min     25th    Median    75th     Max
#   Duration        0.1s    0.2s    0.3s      0.4s     45.0s   <-- skew!
#   Shuffle Read    10 KB   15 KB   20 KB     25 KB    1.2 GB  <-- skew!

How is data skew different from too few partitions?

These two problems look similar (slow stages) but have different signatures and different fixes.

| Characteristic | Data Skew | Too Few Partitions |
|---|---|---|
| Task duration spread | One task is 10-100x the median | All tasks are roughly equally slow |
| Shuffle read spread | One task has a far larger read | All tasks have similar large reads |
| Root cause | Uneven key distribution | Partition count is too low |
| Fix | Salting, AQE skew join, isolate hot key | Increase spark.sql.shuffle.partitions |

PYTHON
# Too few partitions: all tasks are equally large
spark.conf.set("spark.sql.shuffle.partitions", "2")
even_data = spark.range(10000000).withColumn("account_id",
    (col("id") % 1000).cast("string"))
even_data.groupBy("account_id").count().count()
# UI shows 2 tasks, both slow but roughly equal duration

# Data skew: one task is far larger
spark.conf.set("spark.sql.shuffle.partitions", "200")
skewed_txns.groupBy("account_id").sum("amount").count()
# UI shows 200 tasks, 199 fast and 1 extremely slow

What are the consequences of data skew?

The consequences go beyond just slowness. A single straggler task can cause cascading problems in a production banking pipeline.

  • Stage delay – the entire stage waits for the slowest task, so the job runtime equals the straggler runtime regardless of how fast the other tasks finished.
  • Resource waste – executor cores assigned to completed tasks sit idle waiting for the straggler, wasting cluster capacity.
  • Spill to disk – the oversized partition may not fit in the task's memory budget, forcing Spark to spill intermediate data to disk, which slows the task even further.
  • Out-of-memory failure – if the partition is large enough it can exceed the executor's memory entirely, killing the task and potentially the executor.
  • Downstream delays – in an orchestrated pipeline at the National Bank of India, a skewed stage delays every downstream job that depends on its output.
PYTHON
# Demonstrate spill: the straggler task spills to disk
# In the Spark UI Stages tab, check the "Shuffle Spill (Memory)" and
# "Shuffle Spill (Disk)" columns for the straggler task
# A large spill-to-disk value confirms the partition exceeded memory

What are common fixes for data skew?

Several strategies address skew, and the right choice depends on the operation and the data.

  • Adaptive Query Execution (AQE) skew join – Spark 3.0+ can detect skewed partitions at runtime and split them automatically. Enable with spark.sql.adaptive.enabled=true and spark.sql.adaptive.skewJoin.enabled=true.
  • Salting – append a random suffix to the hot key to spread it across multiple partitions, and replicate the other side to match.
  • Isolate and broadcast – filter out the hot key, broadcast-join it separately, and union the results.
  • Pre-aggregation – for groupBy skew, use reduceByKey or partial aggregation to shrink the data before the shuffle.
PYTHON
from pyspark.sql.functions import when, floor, broadcast

# AQE skew join: enable and let Spark handle it automatically
spark.conf.set("spark.sql.adaptive.enabled", "true")
spark.conf.set("spark.sql.adaptive.skewJoin.enabled", "true")
spark.conf.set("spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes",
    "256MB")

# Isolate-and-broadcast approach for a known hot key
hot_txns = skewed_txns.filter(col("account_id") == "CORP_POOL_001")
cold_txns = skewed_txns.filter(col("account_id") != "CORP_POOL_001")

accounts = spark.read.parquet("/data/accounts")

cold_result = cold_txns.join(accounts, "account_id")
hot_result = hot_txns.join(broadcast(accounts), "account_id")
final_result = cold_result.union(hot_result)

How do you monitor for emerging skew over time?

Skew can appear gradually as data patterns change. A personal account that becomes a corporate pooling account, or a new high-volume merchant, can introduce skew that did not exist when the pipeline was first built.

  • Monitor task-duration ratios over time using Spark event logs and the History Server.
  • Set alerts when the max-to-median task duration ratio exceeds a threshold (for example, ten times).
  • Schedule periodic key-distribution profiling jobs that count rows per key and flag new hot keys.
  • Watch for skew in writes too, where a partitionBy on a skewed column produces one gigantic output file.
PYTHON
# Periodic key distribution profiling job
from pyspark.sql.functions import count, desc

def profile_key_distribution(df, key_col, threshold_ratio=100):
    stats = df.groupBy(key_col).agg(count("*").alias("row_count"))
    total_keys = stats.count()
    avg_count = df.count() / total_keys
    hot_keys = stats.filter(col("row_count") > avg_count * threshold_ratio)
    return hot_keys.orderBy(desc("row_count"))

hot = profile_key_distribution(skewed_txns, "account_id")
hot.show()

What about skew in writes?

Skew affects writes as well as reads. When you call df.write.partitionBy("account_id").parquet(path), a skewed account id produces one enormous output file for the hot key's partition directory. This creates the small-files problem in reverse: one giant file that a single downstream task must read entirely.

PYTHON
# Skewed write: one partition directory gets a huge file
skewed_txns.write.partitionBy("account_id").parquet("/output/skewed_write")
# The account_id=CORP_POOL_001 directory will contain a single multi-GB file

# Fix: repartition within each partition to cap file size
skewed_txns.repartition(10, "account_id").write \
    .option("maxRecordsPerFile", 1000000) \
    .partitionBy("account_id").parquet("/output/balanced_write")

What should a senior engineer know about data skew?

  • AQE is the first line of defense – always enable Adaptive Query Execution in Spark 3.0+ before reaching for manual fixes.
  • Null keys are silent killers – a null join key collects every unmatched row into one partition; filter or replace nulls before joining.
  • Skew can hide until data grows – a key that was fine at low volume can become a hot key as the business scales.
  • Salting is not free – it duplicates the small side by the salt factor, so use targeted salting on known hot keys rather than salting everything.
  • Measure before fixing – profile the key distribution and read the Spark UI before applying skew fixes, because the wrong fix wastes effort.

Visual Overview

DIAGRAM
  Before Shuffle (even input)
  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
  │ Part 0  │ │ Part 1  │ │ Part 2  │ │ Part 3  │
  │ 250 rows│ │ 250 rows│ │ 250 rows│ │ 250 rows│
  └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘
       │           │           │           │
       └───────────┴─────┬─────┴───────────┘
                         │ hash(account_id)
                         ▼
  After Shuffle (skewed by hot key)
  ┌──────────────┐ ┌────┐ ┌────┐ ┌────┐
  │   Part 0     │ │ P1 │ │ P2 │ │ P3 │
  │ 10M rows     │ │50  │ │50  │ │50  │
  │ (hot key)    │ │rows│ │rows│ │rows│
  │ STRAGGLER!   │ │    │ │    │ │    │
  └──────────────┘ └────┘ └────┘ └────┘
DIAGRAM
  Spark UI Stages Tab – Detecting Skew
  ┌──────────────────────────────────────────────┐
  │ Task Summary Metrics                         │
  │                                              │
  │ Duration:  Min=0.1s  Median=0.3s  Max=45.0s  │
  │ Shuffle:   Min=10KB  Median=20KB  Max=1.2GB  │
  │                                              │
  │ Timeline:  ████ ████ ████ ████████████████    │
  │            done done done straggler running   │
  └──────────────────────────────────────────────┘

Key Takeaway

Data skew in Spark is when data distributes unevenly across partitions after a shuffle, almost always because a particular join or group key appears disproportionately often. Since Spark runs exactly one task per partition, the oversized partition's task becomes a straggler that delays the entire stage. The rest of the cluster sits idle waiting for that one task to finish, wasting resources and time.

Detection starts in the Spark UI Stages tab. Open the stage after a shuffle and compare the maximum task duration to the median. With skew, the maximum is ten to a hundred times larger, and the straggler task shows a correspondingly large shuffle read. The event timeline confirms it: most tasks finish early and one runs alone. To find the actual hot key, profile the key distribution by grouping on the join or group column and counting rows per key.

In banking pipelines at the National Bank of India, common hot keys include corporate pooling accounts with millions of transactions, suspense accounts collecting error rows, and null keys where every unmatched row piles into one partition. The important distinction is that skew is not the same as too few partitions. Too few partitions makes every task equally large, while skew makes one task large and the rest tiny. The fixes differ: increase partition count for the former, and use AQE skew joins, salting, or isolate-and-broadcast for the latter.

The senior perspective is to enable AQE as the first line of defense, profile key distributions regularly to catch emerging skew, filter or replace null keys before joins, and monitor task-duration ratios over time. Skew can appear gradually as data patterns change, so treat detection as ongoing rather than a one-time fix.

Remember one sentence: data skew is one hot key creating one straggler task that holds up the entire stage, and you detect it in the Spark UI by a maximum task duration wildly exceeding the median.

---

Share
Use ← → arrow keys to navigate