COUNT(DISTINCT)在JOIN后变慢主因是每组需单独维护哈希表或排序结构,导致中间结果爆炸;应改用子查询预聚合,配合复合索引、分区裁剪和合理JOIN顺序优化。

为什么COUNT(DISTINCT)在JOIN后会变慢
不是数据量大导致的慢,而是数据库为每个GROUP BY分组单独维护一个哈希表或排序结构。比如按 channel_name 分组有 50 万组,就要做 50 万次去重——中间结果集爆炸,EXPLAIN 里常出现 Using temporary; Using filesort。
COUNT(DISTINCT)必须上推到子查询预聚合
原始写法让数据库先 JOIN 再去重,极易触发全表扫描和临时表;改成子查询先按关联键聚合,结果集可能从百万级压到几百行,外层只是轻量 JOIN。
- 错误写法:
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 - 子查询能走
dashboard_id + user_id复合索引,外层 JOIN 几乎不走临时表 - 若
table_a按日期分区(如dt),子查询里必须加WHERE dt = '2026-08'提前裁剪,否则分区失效
多字段COUNT(DISTINCT)不能写在一起
同时写 COUNT(DISTINCT user_id), COUNT(DISTINCT order_no) 会让优化器并行维护多个哈希结构,CPU 和内存开销翻倍,执行计划里常看到多个 Distinct 节点。
- 拆成两个独立子查询,再用业务主键(如
channel_code)关联 - 每个子查询都带相同过滤条件(如
del_flag = '0' AND create_date BETWEEN '2026-01-01' AND '2026-06-30'),避免逻辑偏差 - 注意 NULL:若字段允许为空,
COUNT(DISTINCT col)自动忽略;需保留空语义时,改用COUNT(DISTINCT COALESCE(col, -1))
JOIN顺序与索引缺失是隐性加速器
即使用了子查询预聚合,如果子查询里的关联字段没索引,或者 JOIN 时驱动表选错,照样会退化回 Block Nested-Loop,EXPLAIN 显示 type: ALL 或 Using join buffer 就是信号。
-
table_a上至少要有(dashboard_id, user_id)复合索引;若还带时间过滤,索引应为(dashboard_id, dt, user_id) - 子查询里
GROUP BY dashboard_id,外层JOIN table_b时,确保table_b.id有主键或唯一索引,否则外层 JOIN 也可能膨胀 - MySQL 中用
STRAIGHT_JOIN强制左表驱动,避免优化器误判;PostgreSQL 可用 CTE 提升可读性,但语义等价
COUNT(DISTINCT),要连同整个执行路径一起看 EXPLAIN ANALYZE。


















