答案是:90%的“慢join”并非数据倾斜,而是小表未广播、统计信息过期或null值语义污染;真正倾斜表现为极个别task卡在99%、input size异常放大、fuxi instance延迟突增或集中spill/gc;查热点key需结合业务核对兜底值、null影响及真实存储大小;加盐必须双表同步、函数确定、显式重分区。

直接结论:别一上来就加盐,先确认是不是真倾斜、哪张表在拖后腿、热点key长什么样——90% 的“慢 JOIN”根本不是倾斜,而是小表没广播、统计信息过期、或者 NULL 值语义污染。
怎么一眼看出是 JOIN 倾斜,而不是单纯慢?
倾斜不是“所有 task 都慢”,而是“99 个 task 早跑完了,就 1 个卡在 99% 不动”。关键看三类信号:
- Spark UI 中某个 Task 的
Input Size是其他 Task 的 5–100 倍(比如别人 20MB,它 2GB) - MaxCompute Logview 里某 Fuxi Instance 的
Latency明显偏离均值(比如均值 3s,它卡在 45s) - 日志反复出现
Disk spill或GC overhead limit exceeded,且只集中在少数 container
这些现象说明数据在 shuffle 阶段被压到了极少数节点上,不是算力不够,是分布不均。
查热点 key 不能只跑 GROUP BY,得结合业务语义
光执行 SELECT key, COUNT(*) FROM table GROUP BY key ORDER BY COUNT(*) DESC LIMIT 20 只能看到“谁最多”,但看不出“为什么多”。必须人工核对:
- 检查兜底值:
SELECT COUNT(*) FROM table WHERE key IN (0, -1, -999, 'null', '')—— 这些常是 ETL 脚本填的默认值,被事实表大量引用 - 验证空值影响:
SELECT COUNT(*) FROM table WHERE key IS NULL—— Hive/Spark 默认不把NULL当作相同 key,JOIN 时会全丢弃或全打散,行为不一致 - 看真实大小:
DESCRIBE FORMATTED table_name查TotalSize字段,不是行数——STRING 列多的小表可能序列化后超 2GB,根本播不了
加盐必须双表同步、函数确定、分区显式
加盐失效最常见的原因是:只改大表、小表没动;或两边都加了但用的 RAND(),结果同一 key 在两张表里 salt 出不同后缀,JOIN 不上。
正确写法(Spark SQL 示例):
SELECT /*+ REPARTITION(50) */ user_id, SUM(amount) AS total
FROM (
SELECT CASE
WHEN user_id IN (0, -1, -999)
THEN CONCAT(CAST(user_id AS STRING), '_', CAST(hash(user_id) % 5 AS STRING))
ELSE CAST(user_id AS STRING)
END AS user_id, amount
FROM ods_user_events
) t1
JOIN (
SELECT CASE
WHEN user_id IN (0, -1, -999)
THEN CONCAT(CAST(user_id AS STRING), '_', CAST(hash(user_id) % 5 AS STRING))
ELSE CAST(user_id AS STRING)
END AS user_id, user_name
FROM dim_user_info
) t2 ON t1.user_id = t2.user_id;
注意三点:
- 用
hash(user_id) % 5替代RAND() * 5,确保同一 key 在两张表生成相同 salt 后缀 - 盐值个数选 5–20 之间试;太小仍会倾斜,太大徒增 shuffle 数据量
- 必须加
/*+ REPARTITION(50) */或等效 hint,否则 Spark 可能沿用原 key 分区策略,salt 白加
比加盐更轻、更稳的方案:更新统计 + 强制广播
如果小表真实大小 TotalSize),优先走这条路:
- 先跑
ANALYZE TABLE dim_user_info COMPUTE STATISTICS更新统计信息 - SQL 中显式加
/*+ BROADCAST(dim_user_info) */,别依赖spark.sql.autoBroadcastJoinThreshold自动判断 - 若小表含 NULL,JOIN 前统一处理:
COALESCE(user_id, -999)对齐两端空值语义
这招绕开了 shuffle,自然没有倾斜。真正难搞的是那些“看着小、实际大”的维度表——STRING 列多、重复少、序列化后膨胀严重,这种才需要加盐或预聚合。










