最省事的办法是分别对两张表的JOIN字段执行带LIMIT 20的频次统计,重点检查高频key占比是否失衡(如单key>31%或前3和>15%),并排查NULL、默认值等语义污染及类型隐式转换导致的倾斜。

直接查JOIN键的Top N频次分布
最省事的办法,就是对两张表的JOIN字段分别跑一次频次统计,不扫全表,只取前20个高频值。重点不是“有没有大key”,而是看它占总数据量的比例是否失衡。
比如事实表有1200万行,user_id = 0 出现了380万次,那它单key就占了31%——这已经远超安全阈值(前3个key之和>15%就该警觉)。
SELECT user_id, COUNT(*) AS cnt FROM fact_table GROUP BY user_id ORDER BY cnt DESC LIMIT 20SELECT user_id, COUNT(*) AS cnt FROM dim_user GROUP BY user_id ORDER BY cnt DESC LIMIT 20- 必须加
LIMIT,否则可能触发全表扫描拖垮集群 - 别只查事实表:维度表里如果也存在
user_id = 0对应的一条兜底记录,那就是典型的“语义污染型倾斜”
检查NULL、空字符串、默认值这类语义污染key
很多倾斜根本不是业务自然产生,而是ETL填的 -1、'null'、''、'N/A' 这类人工标记,被事实表大量引用后就成了“黑洞”。它们在统计里往往藏得深,因为单独看 COUNT(*) 不显眼,但一JOIN就炸。
- 执行:
SELECT COUNT(*) FROM fact_table WHERE user_id IN (-1, 0, 999999)和SELECT COUNT(*) FROM fact_table WHERE TRIM(CAST(user_id AS STRING)) = '' - 如果结果 > 总行数的5%,且维度表中恰好存在同值记录(比如
dim_user里真有一条user_id = 0),那就基本坐实了 - 注意类型隐式转换陷阱:事实表是字符串型
'0',维度表是整型0,表面JOIN不上,实际可能因隐式转成字符串后又撞上了——这种更难排查
用Spark UI看Task级Input Size差异
SQL统计有采样偏差和缓存干扰,而Spark UI里每个Task的真实Input Size才是硬指标。它绕过了所有中间层,直接告诉你哪几个Task在扛压。
- 进Spark UI → 找到对应JOIN的Stage → 点开Details → 拉到最右看
Input Size列 - 如果最大值 / 中位数 > 5,或某个Task的Input Size > 100GB,基本可断定已倾斜
- 注意区分假象:如果所有Task Input Size都很大且接近,那是分区数太少或资源不足;只有1–2个异常高,才是key分布问题
- 此时再回过头去看那几个慢Task处理的是哪些key,比盲猜高效得多
确认小表是否真的能被广播
很多人以为“小表JOIN大表=自动广播”,但Spark/Hive实际广播的是序列化后的字节大小,不是行数。一张10万行、含长文本字段的维度表,TotalSize 可能飙到2.3GB——超过默认广播阈值(通常2GB),就会退化为Shuffle JOIN,造成“伪倾斜”。
- 执行:
DESCRIBE FORMATTED dim_user,重点关注TotalSize字段(不是numRows) - 若
TotalSize > 2GB,哪怕你加盐也没用,因为根本没走广播路径 - 这时得先压缩小表(比如把长描述字段抽离、用字典编码)或手动指定
BROADCASThint 强制尝试,否则所有优化都是空中楼阁
真正卡住人的,往往是高频key和语义污染混在一起,比如事实表里 user_id = '0' 占40%,维度表却是 user_id = 0(整型),表面类型不匹配,实际在JOIN时被隐式转换后又对上了——这种case光看SQL统计根本发现不了,必须结合UI的Task输入量和字段类型双重交叉验证。










