count(distinct)慢的根源是每分组需独立哈希表去重,10万分组即10万次内存操作;优化须上推至子查询预聚合、用复合索引覆盖过滤与分组字段、多字段去重必须拆解、亿级精确统计应改用approx_count_distinct或物化预计算。

COUNT(DISTINCT)在亿级数据上慢,不是SQL写错了,而是数据库必须为每个分组建独立哈希表——10万分组 = 10万次内存去重,不爆才怪。
为什么EXPLAIN看到Using temporary; Using filesort就该警觉
这代表数据库正在磁盘上建临时表做去重,性能已断崖下跌。根本原因不是数据多,而是执行计划把COUNT(DISTINCT)放在JOIN或GROUP BY之后才触发,导致中间结果集爆炸。
- 原始写法:
SELECT b.name, COUNT(DISTINCT a.user_id) FROM table_a a JOIN table_b b ON a.dashboard_id = b.id GROUP BY b.name→ 先JOIN出几百万行,再对每组name做去重 - 优化后:
SELECT b.name, new_a.ct FROM table_b b JOIN (SELECT dashboard_id, COUNT(DISTINCT user_id) AS ct FROM table_a GROUP BY dashboard_id) new_a ON new_a.dashboard_id = b.id→ 子查询只产出几百行聚合结果,JOIN极轻量 - 务必在子查询里加
WHERE dt = '2026-08'(如果表按日期分区),否则分区裁剪失效,照样扫全表
多个COUNT(DISTINCT)字段必须拆开预聚合
写成COUNT(DISTINCT user_id), COUNT(DISTINCT order_no)会让执行引擎并行维护两个哈希结构,CPU和内存开销翻倍,Shuffle数据量也可能暴涨。
- 错误示范:
SELECT channel_code, COUNT(DISTINCT user_id), COUNT(DISTINCT order_no) FROM user_order GROUP BY channel_code - 正确做法:两个独立子查询分别聚合,再用
channel_code关联:SELECT t1.channel, t1.user_cnt, t2.order_cnt FROM (SELECT channel_code AS channel, COUNT(DISTINCT user_id) AS user_cnt FROM user_order WHERE ... GROUP BY channel_code) t1 JOIN (SELECT channel_code AS channel, COUNT(DISTINCT order_no) AS order_cnt FROM user_order WHERE ... GROUP BY channel_code) t2 ON t1.channel = t2.channel - Spark SQL中更危险:它会插入
Expand节点,1行原始数据变成N行(N=去重字段数),30个字段就是30倍Shuffle流量
索引对COUNT(DISTINCT)几乎没用,但复合索引能救命
单列索引如INDEX(user_id)对COUNT(DISTINCT user_id)基本无效——因为去重必须加载所有非NULL值进内存比对,索引无法跳过这个过程。
- 真正起作用的是覆盖型复合索引,例如查询
SELECT COUNT(DISTINCT user_id) FROM t WHERE dt = '2026-08-01',建INDEX(dt, user_id)可避免回表,且让WHERE条件走索引快速定位数据范围 - 如果还有
GROUP BY channel,索引应为(dt, channel, user_id),前导列顺序必须匹配过滤+分组+去重字段 - MySQL中
innodb_buffer_pool_size太小、PostgreSQL中work_mem设太低,都会导致哈希表溢出落盘,直接I/O卡死
要不要精确?这是架构问题,不是SQL调优问题
万亿级日志查COUNT(DISTINCT uid)还要求100%准确,等于逼数据库硬扛GB级哈希表——这不是调参能解决的。
- 接受±1%误差?用
APPROX_COUNT_DISTINCT(PostgreSQL)、uniq()(ClickHouse)、HLL(Spark)等概率算法,内存占用恒定KB级 - 必须精确?提前物化:每天跑一次
INSERT INTO daily_uv SELECT dt, COUNT(DISTINCT user_id) FROM raw_log WHERE dt = '2026-08-02',查时直接SELECT uv FROM daily_uv WHERE dt = '2026-08-02' - 别忘了NULL值:
COUNT(DISTINCT col)自动忽略NULL,但若业务逻辑需要统计“空值也算一种状态”,就得改成COUNT(DISTINCT COALESCE(col, 'NULL'))











