count(distinct)触发单点reduce瓶颈,因其编译为单个mr作业,所有相同值被shuffle至同一reduce去重计数;null值、测试账号及热点id易引发倾斜,可通过group by预去重+count(*)子查询拆解两阶段解决。

为什么COUNT(DISTINCT)会触发单点Reduce瓶颈
因为 COUNT(DISTINCT) 在 Hive/Spark SQL 中本质是两阶段聚合:先按 DISTINCT 字段分组去重,再统计行数。所有相同值(比如大量 user_id = 'null' 或热门商品ID)会被 shuffle 到同一个 Reduce,导致该任务处理数据量远超其他 Reduce。
Hive 中 COUNT(DISTINCT) 的执行计划暴露了问题
Hive 默认把 COUNT(DISTINCT) 编译成一个 MapReduce 作业,其中 Reduce 数由 mapred.reduce.tasks 决定,但 key 分布不受控——没有预打散逻辑,也没有 fallback 机制。常见现象是:任务卡在 99%,监控里只剩 1–2 个 Reduce 长时间运行,hive.groupby.skewindata=true 对 COUNT(DISTINCT) 无效(它只作用于 GROUP BY,不覆盖 DISTINCT 聚合)。
哪些值最容易引发倾斜?怎么快速识别
容易倾斜的值有三类:
• NULL 值集中(如未登录用户日志中 user_id 大量为 NULL)
• 固定业务标识(如测试账号 'test_001'、埋点默认值 'unknown')
• 真实热点(如大促期间头部商品 ID 出现频次超千万)
验证方法:先跑 SELECT user_id, COUNT(*) FROM logs GROUP BY user_id ORDER BY COUNT(*) DESC LIMIT 10,看 top10 是否占全量 30% 以上。
改写为子查询 + GROUP BY 是最稳妥的绕过方式
把去重和计数拆成两个物理阶段,让 shuffle 分布可控:
• 原写法:SELECT COUNT(DISTINCT user_id) FROM logs
• 改写后:SELECT COUNT(*) FROM (SELECT user_id FROM logs GROUP BY user_id) t
这样第一层 GROUP BY 可启用 hive.groupby.skewindata=true,第二层只是轻量计数;若仍倾斜,再加盐:SELECT COUNT(*) FROM (SELECT CONCAT(user_id, '_', CAST(FLOOR(RAND() * 100) AS STRING)) AS salted_id FROM logs GROUP BY user_id, salted_id) t。注意盐值范围别设太大(如 1000),否则小 key 的 group by 开销反而上升。











