COUNT(DISTINCT) 耗时长的根本原因是数据库需为每组维护独立哈希表或排序结构;优化核心是将去重上推至JOIN前的子查询预聚合,并配合覆盖索引与严格过滤条件。

COUNT(DISTINCT) 耗时极长,根本原因不是数据多,而是数据库被迫为每个分组维护独立的哈希表或排序结构——比如 GROUP BY channel 有 10 万组,就得做 10 万次去重操作。
为什么 COUNT(DISTINCT) 容易触发 Using temporary; Using filesort
MySQL 在执行 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会让table_a和table_b先全量关联,再对每组 name 做去重 —— 若table_b有 10 万行,中间结果可能达千万级 - EXPLAIN 中出现
Using temporary; Using filesort就是明确信号:MySQL 正在内存建哈希表,撑不住就落磁盘 - 多字段组合去重(如
COUNT(DISTINCT user_id), COUNT(DISTINCT order_no))会强制并行维护多个哈希结构,CPU 和内存开销翻倍
必须把去重操作“上推”到子查询里预聚合
核心优化动作:让去重发生在 JOIN 之前,把大表压缩成小结果集再关联。这不是语法糖,是执行计划质变的关键:
- 把
table_a按dashboard_id预聚合一次:SELECT dashboard_id, COUNT(DISTINCT user_id) AS ct FROM table_a WHERE dt = '2026-07' GROUP BY dashboard_id—— 结果可能只有几百行 - 这个子查询能直接命中
(dashboard_id, user_id)复合索引,避免回表和排序 - 外层只 JOIN 几百行,基本不走临时表;WHERE 中的
dt = '2026-07'还能触发分区裁剪(如果表按dt分区) - 多字段去重必须拆开:两个独立子查询分别按
channel_code聚合user_id和order_no,再用channel_code关联,别写在一个 SELECT 里
索引设计必须覆盖去重字段和过滤条件
没有合适索引,预聚合也白搭。复合索引顺序和内容决定是否真正生效:
- 对
COUNT(DISTINCT user_id) GROUP BY dashboard_id,索引必须是(dashboard_id, user_id),反过来无效 - 如果查询带
WHERE del_flag = '0' AND create_date BETWEEN '2026-01-01' AND '2026-06-30',索引应扩展为(del_flag, create_date, dashboard_id, user_id),确保 WHERE 和 GROUP BY + DISTINCT 全部覆盖 - 避免在 TEXT/BLOB 或超长 VARCHAR 上 COUNT(DISTINCT);真要处理长内容,先存 MD5 哈希值,再对
content_hash建索引 - NULL 值过多会削弱索引效率,关键去重字段建议设
NOT NULL
最容易被忽略的一点:预聚合子查询里的 WHERE 条件,必须和业务实际时间范围严格一致。漏掉 dt 或 create_date 过滤,子查询仍会扫全表 —— 看似改了写法,性能毫无改善。


















