COUNT(DISTINCT)在GROUP BY中慢是因为SQL Server对每个分组单独去重,5万组即执行5万次哈希/排序;内存不足时溢出TempDB导致性能断崖下跌,应改用子查询预聚合并加分区裁剪和覆盖索引优化。

为什么COUNT(DISTINCT)在GROUP BY里特别慢
不是数据总量大导致慢,而是SQL Server对每个分组都单独做一次去重——比如按channel_code分组后算COUNT(DISTINCT user_id),若有5万组,就得执行5万次哈希或排序。执行计划中若出现Using temporary; Using filesort(实际在SQL Server里表现为Hash Match (Aggregate) + Spill to TempDB),基本意味着内存撑不住,开始往磁盘写临时数据,性能断崖下跌。
用子查询预聚合替代JOIN后去重
核心思路是把高成本的COUNT(DISTINCT)下沉到子查询中,在更小的结果集上完成去重,外层只做轻量关联。原始低效写法:
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 WHERE dt = '2026-09' -- 必须加分区裁剪 GROUP BY dashboard_id ) new_a ON new_a.dashboard_id = b.id
- 子查询
table_a先按dashboard_id聚合,结果行数通常极小(几百到几千),JOIN开销远低于原写法 -
WHERE dt = '2026-09'必须写在子查询内,否则优化器无法下推谓词,仍会扫描全表 - 若
table_a有覆盖索引(如(dashboard_id, user_id, dt)),子查询可完全走索引,避免回表
多个DISTINCT字段必须拆开预聚合
同时写COUNT(DISTINCT col1), COUNT(DISTINCT col2)会让SQL Server生成多个独立的哈希聚合通道,CPU和TempDB压力翻倍。不能靠一个子查询解决,必须分别聚合再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 >= '2026-01-01' 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 >= '2026-01-01' GROUP BY channel_code ) t2 ON t1.channel = t2.channel
注意两点:
- 两个子查询的
WHERE条件必须完全一致,否则JOIN结果可能出错 - 如果
user_order表很大,建议给(channel_code, del_flag, create_date)建复合索引,并把order_no和user_id作为包含列(INCLUDE)以支持覆盖扫描
别让DISTINCT掩盖主键或唯一约束
如果去重字段本身就是主键或有唯一约束(比如COUNT(DISTINCT order_id)),SQL Server仍会走完整去重流程,白白浪费资源。此时应直接用COUNT(*)替代:
-- 低效(order_id是主键) SELECT COUNT(DISTINCT order_id) FROM orders WHERE status = 'shipped' <p>-- 高效等价写法 SELECT COUNT(*) FROM orders WHERE status = 'shipped'
这类误用在视图或ORM生成SQL中很常见,容易被忽略——它不报错,但执行计划里多出不必要的Hash Aggregate节点,且无法利用统计信息跳过计算。

















