优化count(distinct)需将去重操作上推至子查询预聚合,避免join后爆炸式中间结果;结合复合索引、分区裁剪、拆分多字段去重,并确保索引覆盖以规避全表扫描。

COUNT(DISTINCT) 在大数据量下容易变慢,不是因为数据多,而是数据库对每个分组都要独立建哈希表或排序——比如 GROUP BY channel 有 10 万组,就得做 10 万次去重。优化核心是:把去重操作从 JOIN 后“上推”到子查询里提前完成。
先聚合再 JOIN,别让 COUNT(DISTINCT) 扫全表
原始写法会让数据库先 JOIN 再分组去重,中间结果集爆炸,极易触发 Using temporary; Using filesort:
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
改成子查询预聚合后,table_a 只按 dashboard_id 聚一次,结果可能只有几百行:
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
- 子查询能直接利用
dashboard_id+user_id的复合索引 - 如果
table_a按日期分区(如dt),必须在子查询里加WHERE dt = '2026-07'提前裁剪 - 外层
JOIN对象极小,基本不走临时表
多字段 COUNT(DISTINCT) 必须拆开算
写成 COUNT(DISTINCT user_id), COUNT(DISTINCT order_no) 看似简洁,实际会强制数据库并行维护多个哈希结构,CPU 和内存开销翻倍。
正确做法是各自独立预聚合,再用 channel_code 关联:
SELECT t1.channel, t1.order_cnt, t2.user_cnt FROM ( SELECT channel_code AS channel, COUNT(DISTINCT order_no) AS order_cnt FROM user_order WHERE del_flag = '0' AND create_date BETWEEN '2026-01-01' AND '2026-06-30' GROUP BY channel_code ) t1 JOIN ( SELECT channel_code AS channel, COUNT(DISTINCT user_id) AS user_cnt FROM user_order WHERE del_flag = '0' AND create_date BETWEEN '2026-01-01' AND '2026-06-30' GROUP BY channel_code ) t2 ON t1.channel = t2.channel
- 两个子查询可分别走
(channel_code, order_no)和(channel_code, user_id)索引 - 避免在同一个扫描中维护多套去重状态,降低 OOM 风险
- 若字段组合本身高基数(如含时间戳),先确认是否真需要该粒度——有时业务上可降级为天级去重
检查 GROUP BY 是否混入了高粒度字段
常见错误是 GROUP BY name, order_id, created_at,导致每组只有一两行,COUNT(DISTINCT user_id) 自然接近 1。
- 用
SELECT COUNT(*), COUNT(DISTINCT user_id)对比同一组的总数和去重数,差值过大就说明分组过细 - MySQL 8.0+ 在
ONLY_FULL_GROUP_BY模式下会直接报错,但即使不报错,结果也已失真 - 字符串列若用了前缀索引(如
INDEX(name(10))),COUNT(DISTINCT name)实际只基于前 10 字符去重,需检查是否覆盖业务需求
最易被忽略的一点:COUNT(DISTINCT) 在无索引列上执行时,哪怕只是单表统计,也可能全程走全表扫描 + 内存哈希;而加索引后,InnoDB 有时能直接遍历索引 B+ 树叶子节点完成计数——所以先看 EXPLAIN 里有没有用上索引,再谈其他优化。











