count(distinct) + group by变慢主因是数据库为每个分组独立执行去重操作,如10万channel需做10万次哈希/排序;explain出现using temporary; using filesort表明磁盘建临时表,性能断崖下跌;应改用先聚合再join,将去重下沉至子查询,外层仅轻量关联,并配合分区裁剪与覆盖索引优化。

为什么COUNT(DISTINCT) + GROUP BY容易变慢
不是数据量大本身导致慢,而是数据库对每个分组都要独立执行一次去重——比如 COUNT(DISTINCT user_id) 在 10 万个 channel 分组里,就得做 10 万次哈希或排序。EXPLAIN 中若出现 Using temporary; Using filesort,基本等于在磁盘上建临时表,性能断崖式下跌。
先聚合再 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
优化后:
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
- 子查询
table_a按dashboard_id聚合,结果集极小(比如几百行),JOIN 快得多 - 避免在 JOIN 后才触发
COUNT(DISTINCT),否则优化器大概率放弃索引 - 如果
table_a有分区(如按dt),务必在子查询里加WHERE dt = '2026-06'提前裁剪
多字段COUNT(DISTINCT)必须拆开算
同时写 COUNT(DISTINCT col1), COUNT(DISTINCT col2) 会让执行计划生成多个独立去重通道,CPU 和内存开销翻倍。正确做法是分别预聚合再 JOIN:
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)) - 别试图用
UNION ALL合并后再GROUP BY,那只会放大 shuffle 数据量
索引怎么建才真正生效
COUNT(DISTINCT) 和 GROUP BY 共享同一套底层机制:排序或哈希。索引不是“有了就行”,得匹配访问模式:
- 单字段去重:如
SELECT COUNT(DISTINCT status) FROM orders→ 建INDEX(status) - 多字段组合分组+去重:如
GROUP BY channel, DATE_FORMAT(create_date,'%Y')→ 建INDEX(channel, create_date),create_date必须是原生字段,函数不能走索引 - 覆盖索引场景:如
SELECT channel, COUNT(DISTINCT user_id) FROM orders WHERE dt = '2026-06' GROUP BY channel→ 建INDEX(dt, channel, user_id),避免回表
最常被忽略的是:GROUP BY 字段顺序必须和联合索引前导列一致,否则索引直接失效。











