When working with large datasets in Apache Spark or Azure Databricks, you may encounter a situation where a job looks almost finished, but a few tasks continue running for a long time.
One common reason is Data Skew.
In this article, we’ll understand what data skew is, why it affects Spark performance, and how salting can help solve it.
What is Data Skew?
Spark processes large datasets by dividing them into smaller pieces called partitions.
Ideally, the data should be reasonably balanced across those partitions.
For example:
Partition 1 → 10 million rows
Partition 2 → 11 million rows
Partition 3 → 9 million rows
Partition 4 → 10 million rows
The workload is fairly balanced, so Spark workers can process the partitions in roughly the same amount of time.
Now imagine this:
Partition 1 → 5 million rows
Partition 2 → 5 million rows
Partition 3 → 5 million rows
Partition 4 → 80 million rows
Partition 4 has significantly more work than the others.
The first three tasks may finish quickly, while the task processing Partition 4 continues running.
The entire Spark stage has to wait for that slow task.
This is called Data Skew.
In simple terms:
Data Skew = Uneven data distribution → Uneven workload → Slow Spark job
A Realistic Example
Suppose we have a large transaction dataset containing customer transactions by country.
The data distribution looks like this:
UAE → 80 million rows
Pakistan → 5 million rows
India → 5 million rows
UK → 2 million rows
We also have a small country reference table:
country region
---------------------
UAE Middle East
Pakistan South Asia
India South Asia
UK Europe
Now we perform a join:
result = transactions_df.join(
country_df,
"country"
)
The SQL equivalent would conceptually be:
SELECT *
FROM transactions t
JOIN countries c
ON t.country = c.country;
The problem is that the join key is highly uneven.
UAE appears 80 million times, while the other countries appear far less frequently.
During a normal shuffle join, records with the same join key need to be brought together.
This means the UAE key can create a very large workload for a small number of tasks.
That is our skew problem.
How Can We Detect Data Skew?
One simple check is to examine the distribution of the join key.
transactions_df \
.groupBy("country") \
.count() \
.orderBy("count", ascending=False) \
.show()
We might see:
+----------+----------+
| country | count |
+----------+----------+
| UAE | 80000000 |
| Pakistan | 5000000 |
| India | 5000000 |
| UK | 2000000 |
+----------+----------+
This immediately tells us that UAE is a heavily skewed key.
In a real production environment, I would also inspect the Spark UI.
I would look for signs such as:
- A few tasks taking much longer than others
- Very uneven task input sizes
- Large shuffle reads/writes
- Most tasks finishing while a small number remain active
Solution: Salting
One technique for handling heavily skewed join keys is called salting.
The idea is surprisingly simple.
Instead of allowing all 80 million UAE records to use the same join key:
UAE
we artificially divide them into multiple keys:
UAE_0
UAE_1
UAE_2
UAE_3
...
UAE_9
If the distribution is reasonably even, our 80 million UAE records can now be split roughly like:
UAE_0 → ~8 million
UAE_1 → ~8 million
UAE_2 → ~8 million
...
UAE_9 → ~8 million
Instead of one extremely large group, Spark has several smaller groups that can be processed in parallel.
The artificial number we add is called the salt.
Step 1: Add Salt to the Large Dataset
Let’s add a salt value between 0 and 9.
from pyspark.sql.functions import rand, floor
transactions_salted = transactions_df.withColumn(
"salt",
floor(rand() * 10)
)
Our data may now look like:
transaction_id country salt
----------------------------------
10001 UAE 3
10002 UAE 7
10003 UAE 1
10004 UAE 5
10005 Pakistan 2
We can then create a salted join key:
from pyspark.sql.functions import concat, col, lit
transactions_salted = transactions_salted.withColumn(
"salted_country",
concat(
col("country"),
lit("_"),
col("salt")
)
)
Now we have:
country salt salted_country
------------------------------------
UAE 3 UAE_3
UAE 7 UAE_7
UAE 1 UAE_1
Pakistan 2 Pakistan_2
The UAE records are no longer represented by only one join key.
But There Is a Problem
Our small country table still contains:
UAE
Pakistan
India
UK
But the large dataset now contains:
UAE_0
UAE_1
UAE_2
...
UAE_9
If we join these directly, they won’t match.
Therefore, we need to prepare the smaller side of the join as well.
Step 2: Replicate the Small Table
We create salt values from 0 through 9:
from pyspark.sql.functions import explode, sequence
country_salted = country_df.withColumn(
"salt",
explode(sequence(lit(0), lit(9)))
)
The original UAE row:
UAE → Middle East
becomes:
UAE 0 Middle East
UAE 1 Middle East
UAE 2 Middle East
UAE 3 Middle East
...
UAE 9 Middle East
Then we create the same salted key:
country_salted = country_salted.withColumn(
"salted_country",
concat(
col("country"),
lit("_"),
col("salt")
)
)
Now both sides contain compatible keys:
Large dataset Small dataset
UAE_0 UAE_0
UAE_1 UAE_1
UAE_2 UAE_2
...
UAE_9 UAE_9
Step 3: Perform the Salted Join
Now we can join using the salted key:
result = transactions_salted.join(
country_salted,
"salted_country"
)
Conceptually, instead of:
80 million UAE records
↓
UAE
↓
Huge workload
we have:
80 million UAE records
↓
┌────────┼────────┐
↓ ↓ ↓
UAE_0 UAE_1 ... UAE_9
↓ ↓ ↓
Task Task Task
The workload can now be distributed across more tasks.
Do We Always Need Salting?
No.
This is an important point.
Salting is useful, but it adds complexity and can increase the size of the smaller dataset because rows must be replicated.
Before implementing salting, investigate the actual cause of the performance problem.
If the second table is very small, a broadcast join may be a much simpler solution.
For example:
from pyspark.sql.functions import broadcast
result = transactions_df.join(
broadcast(country_df),
"country"
)
Spark sends the small country_df to the executors.
This can avoid the large shuffle that caused the skew problem in the first place.
So for our example:
transactions_df → 92 million rows
country_df → 200 rows
I would normally investigate a broadcast join before implementing salting.
Broadcast Join vs Salting
A useful rule of thumb is:
Small lookup table
↓
Consider Broadcast Join first
Large-to-large join
+
Highly skewed join key
↓
Consider Salting
Salting becomes particularly useful when broadcasting isn’t practical and a small number of keys dominate the dataset.
What About AQE?
Modern Spark also provides Adaptive Query Execution (AQE).
AQE can dynamically optimize query execution based on runtime statistics and includes capabilities for handling some skewed joins.
Therefore, in a production environment, I wouldn’t immediately start writing salting logic.
I would first:
1. Identify the slow stage
↓
2. Inspect Spark UI
↓
3. Check key distribution
↓
4. Check shuffle and partition sizes
↓
5. Consider AQE
↓
6. Consider Broadcast Join
↓
7. Repartition if appropriate
↓
8. Use Salting when necessary
Interview Question
Interviewer: What is Data Skew?
A strong short answer is:
“Data skew occurs when data is unevenly distributed across Spark partitions. Some partitions become much larger than others, causing a few tasks to take significantly longer and slowing down the entire Spark stage.”
Interviewer: How would you solve it?
“First I would confirm the skew by checking key distribution and the Spark UI. Depending on the situation, I could use AQE, a broadcast join when one side is small, repartitioning, or salting for heavily skewed keys.”
Interviewer: Explain salting.
“Salting means adding an artificial value to a heavily skewed join key so that records for that key are distributed across multiple partitions. On the other side of the join, we create matching salted keys. This allows Spark to process the skewed key across multiple tasks instead of creating one large bottleneck.”
Final Takeaway
Remember this sequence:
Data Skew
↓
Uneven key distribution
↓
Uneven partitions
↓
Few tasks become very large
↓
Spark stage becomes slow
And the salting solution:
UAE → 80M rows
↓
SALTING
↓
UAE_0 UAE_1 UAE_2 ... UAE_9
↓ ↓ ↓ ↓
Task Task Task Task
The most important lesson is:
Salting doesn’t reduce the amount of data. It redistributes the work so Spark can process a heavily skewed key more evenly in parallel.
For a Senior Data Engineer interview, don’t simply say, “I use salting for skew.”
Explain the decision:
Detect the skew → understand the cause → choose the simplest appropriate optimization → validate the performance improvement.
That demonstrates senior-level problem solving rather than just knowledge of Spark terminology.
This can also become a strong Data Engineering blog/LinkedIn article and fits well with your plan to create educational Data Engineering content.