Detecting skew- Compute key frequency on the large fact table and inspect heavy hitters:fact.groupBy("join_key").count().orderBy(desc("count")).limit(20).show()
- Observe shuffle/read metrics in Spark UI: a few tasks with very large shuffleRead or long duration indicate skew.- Check partition sizes: rdd.mapPartitions(lambda it: [sum(1 for _ in it)]).collect() shows uneven counts.Three concrete mitigation techniques1) Broadcast the small dimension (map-side join)When the dimension is truly small (< spark.sql.autoBroadcastJoinThreshold or manually set), broadcast eliminates shuffle.from pyspark.sql.functions import broadcast
joined = fact.join(broadcast(dim), "join_key")
Config: spark.conf.set("spark.sql.autoBroadcastJoinThreshold", 10*1024*1024) or use hint: fact.join(dim.hint("broadcast"), "join_key")2) Salting hot keysIf a few keys dominate, add a random salt to both sides for hot keys so their work is spread across partitions, then aggregate to remove salts.from pyspark.sql.functions import when, rand, floor, concat, lit
num_salts = 10
fact2 = fact.withColumn("salt", when(col("join_key").isin(hot_keys), floor(rand()*num_salts)).otherwise(lit(0)))
fact2 = fact2.withColumn("join_key_salted", concat(col("join_key"), lit("_"), col("salt")))
dim2 = dim.withColumn("salt", when(col("join_key").isin(hot_keys), explode(array([lit(i) for i in range(num_salts)]))).otherwise(lit(0)))
dim2 = dim2.withColumn("join_key_salted", concat(col("join_key"), lit("_"), col("salt")))
joined = fact2.join(dim2, "join_key_salted")
# then aggregate to remove salt
Reason: spreads heavy-key rows across num_salts partitions.3) Repartition / skew-aware partitioning and adaptive execution- Repartition the fact table by join key to balance partitions: fact.repartition(200, "join_key")- Use Spark Adaptive Query Execution (AQE) to automatically handle skewed partitions:spark.conf.set("spark.sql.adaptive.enabled", "true")
spark.conf.set("spark.sql.adaptive.skewJoin.enabled", "true")
AQE will split skewed partitions at runtime. Also tune shuffle partitions: spark.conf.set("spark.sql.shuffle.partitions", "500")Trade-offs and notes- Broadcasting uses memory on executors; ensure dim fits.- Salting increases data volume and requires post-aggregation.- Repartitioning costs a shuffle; choose when join is repeated downstream.- Use Spark UI to verify improvements (reduced max task time, more even shuffleRead).