在大规模数据量的SQL系统中,明明建了索引、加了资源,某些聚合或关联查询却依旧长时间跑不出结果。翻看执行计划往往会发现,个别处理节点承担了绝大多数数据,其余节点早早空闲,整体进度被极少数任务卡死。这种现象背后通常不是语法问题,而是数据倾斜——即少数键值对应的记录量远超其他键值,破坏了并行计算的负载均衡。

数据倾斜的成因与典型识别方式
数据倾斜的本质是数据分布函数未能将记录均匀映射到计算单元。以用户行为日志为例,若某几个重度活跃用户的操作频次是普通用户的千倍,那么以用户ID作为分组或连接键时,这几个ID就会形成数据热点。在MapReduce或MPP架构里,负责这些热点的线程必须处理海量数据,而其他线程处理完少量数据后只能等待,从而出现长尾效应。
识别倾斜不能只靠肉眼翻表,需要结合执行引擎的统计信息。在Hive或Spark SQL中,可以通过查看每个task处理的行数分布来判断;在MySQL这种单机数据库中,虽无分布式task概念,但某些等值查询因索引区分度低也会造成类似倾斜,例如用性别字段做分区键。下面是一段在Spark SQL中检查各分组记录数的诊断语句,通过对比最大值与平均值就能快速定位热点键:
SELECT user_id,
COUNT(*) AS cnt
FROM user_action_log
GROUP BY user_id
ORDER BY cnt DESC
LIMIT 20;
拿到结果后,如果排名第一的user_id记录数超过总表量的百分之十,且远大于第二名的数值,基本可以确认存在严重倾斜。此时无论怎么调大内存或并发数,都只是缓解而非根治,必须回到数据分布本身做优化。
哈希分桶与加盐打散的优化实践
面对倾斜最常用的方法是加盐打散,也就是给原本的热键拼接一个随机前缀,把集中流量拆成多份。以刚刚的user_action_log为例,我们可以在写入或查询时生成user_id加随机数的新键,让同一个重度用户的数据分散到不同桶中计算,最后再按原user_id二次聚合。这种写法在ETL层改造后,能将单点压力降到原来的数十分之一。
具体实现上,如果使用的是支持分桶表的引擎,推荐在建表时指定分桶字段和数量,利用哈希函数天然均衡分布。如下示例展示Hive中按user_id哈希分入三十二个桶的表定义,引擎会依据哈希值决定记录落点,大幅降低人为热点:
CREATE TABLE user_action_bucketed (
user_id STRING,
action STRING,
ts BIGINT
)
CLUSTERED BY (user_id) INTO 32 BUCKETS
STORED AS ORC;
若无法改动存储结构,也可以在查询时动态加盐。下面这段SQL给每个user_id附加零到九的随机后缀,先局部聚合再去掉后缀全局聚合,从而绕开单键拥堵。需要注意随机数范围要和实际并发能力匹配,过小打散不彻底,过大则带来过多小文件问题:
SELECT raw_user_id,
SUM(part_cnt) AS total_cnt
FROM (
SELECT SUBSTR(user_id, 1, LENGTH(user_id) - 2) AS raw_user_id,
COUNT(*) AS part_cnt
FROM (
SELECT CONCAT(user_id, '_', CAST(FLOOR(RAND() * 10) AS STRING)) AS user_id
FROM user_action_log
) t1
GROUP BY user_id
) t2
GROUP BY raw_user_id;
加盐方案虽有效,但会增加一次额外的shuffle和聚合,因此只建议在确认倾斜后再使用。对于区分度本身就高的字段,直接哈希分桶已足够,不必引入随机维度,否则反而拖慢正常查询。
分区裁剪与广播连接的协同策略
除了打散热点,合理运用分区裁剪能从源头减少参与计算的数据量。将大表按时间或地域做静态分区,使SQL优化器只扫描相关目录,可避免全表扫描放大倾斜影响。例如订单表按天分区后,分析某日数据时仅加载当日文件,热点用户即使数据多也局限于单日范围内,不会波及历史全量。
当倾斜发生在大小表关联时,广播小表是更轻量的解法。将维表或配置表直接分发到每个计算节点内存中,避免以倾斜键做shuffle。以下Spark SQL提示符可强制广播,省去大表按键重分布的代价:
SELECT /*+ BROADCAST(dim_user) */
f.user_id,
dim_user.user_name,
COUNT(*) AS pv
FROM fact_log f
JOIN dim_user
ON f.user_id = dim_user.user_id
GROUP BY f.user_id, dim_user.user_name;
不过广播不适用于小表本身也存在倾斜且被大表高频命中的情况,那时仍需结合加盐。工程上通常先通过监控面板捕捉慢查询指纹,再针对性选择分区裁剪、分桶或广播其中一种或组合使用。只有把数据分布当作设计的一等公民,而非上线后的补救项,SQL性能才具备长期稳定性。