COUNT(DISTINCT)比COUNT()慢3–10倍,因其需维护哈希表或排序缓冲区去重,而COUNT()仅单次扫描累加;高基数字段加剧内存膨胀,且混用DISTINCT与GROUP BY、子查询或JOIN会触发双重临时表、物化全量结果或掩盖真实性能缺陷。

为什么COUNT(DISTINCT)比COUNT(*)慢3–10倍
DISTINCT不是轻量级修饰符,而是强制数据库启动一套独立的去重流程。COUNT(DISTINCT user_id)实际分两步:先为每个分组维护一个哈希表(或排序缓冲区)存所有非NULL的user_id值,再对这个结构计数。而COUNT(*)只需单次扫描、累加计数器,连内存分配都省了。
常见错误现象:Using temporary和Using filesort频繁出现在EXPLAIN里;内存占用陡增,tmp_table_size或sort_buffer_size被撑爆,触发磁盘临时文件;同样数据量下响应时间跳变。
- 字段基数越高(如user_id上千万唯一值),哈希表膨胀越剧烈
-
COUNT(DISTINCT)自动忽略NULL,但业务若把NULL视作有效状态(如“未登录用户”),结果就错漏 - MySQL 8.0+对
COUNT(DISTINCT)支持哈希聚合,但前提是没混用GROUP BY或嵌套子查询,否则退化回排序路径
DISTINCT在GROUP BY中触发双重临时表
SQL标准禁止DISTINCT与GROUP BY共存,强行写会绕过语法检查,但执行时优化器彻底失能:先按GROUP BY分组生成中间结果,再对整行去重——等于扫两次表、建两次临时结构。
典型错误写法:SELECT DISTINCT dept_id, COUNT(*) FROM t GROUP BY dept_id。这不是语义冗余,是逻辑冲突:GROUP BY已保证dept_id唯一,DISTINCT纯属多余,却让数据库多做一次全量行比较。
- 真正需要的是“每个部门有多少独立用户”,应写成
SELECT dept_id, COUNT(DISTINCT user_id) FROM t GROUP BY dept_id,而非套DISTINCT - 若分组键本身来自JOIN结果(如
dept_name, region),必须确保底层有对应联合索引,否则GROUP BY阶段就已触发Using temporary - MySQL低版本在严格模式关闭时允许该写法,但执行计划里
Handler_read_rnd_next飙升,说明在随机回表读取
嵌套查询里加DISTINCT等于关掉所有优化通道
一旦子查询或CTE里出现DISTINCT,多数引擎(MySQL/PostgreSQL)立刻放弃谓词下推、索引利用和流式处理,强制物化全部中间结果。哪怕外层只LIMIT 10,也得先把子查询的50万行全算出来再截断。
比如视图定义含SELECT DISTINCT a, b FROM t JOIN s ON ...,调用时加WHERE a = 'x',MySQL仍走TEMPTABLE算法——先物化整个JOIN结果,再过滤,完全绕过a上的索引。
- 用
EXISTS替代IN (SELECT DISTINCT ...),避免子查询膨胀 - 子查询若必须去重,优先建覆盖索引:
INDEX(status, category)支撑SELECT DISTINCT status, category FROM orders - PostgreSQL中
work_mem不足时,DISTINCT直接落盘,嵌套层级越多,溢出概率越大
JOIN后硬加DISTINCT掩盖真正的性能缺陷
SELECT DISTINCT u.id, u.name FROM users u JOIN orders o ON u.id = o.user_id看着简洁,实则危险:它默认对连接后的整行去重,而一对多JOIN会让1000个用户变成5000行中间结果——DISTINCT不是在“用户维度”去重,是在“用户+订单组合”维度去重。
更糟的是,这掩盖了JOIN设计问题:你到底需不需要订单字段?需要最新一条还是汇总数据?硬加DISTINCT只是让错误逻辑“出得来结果”,但代价是I/O和CPU双飙升。
- 要确认用户是否存在订单,改用
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id) - 要取每个用户的最新订单,用
ROW_NUMBER() OVER (PARTITION BY u.id ORDER BY o.created_at DESC),然后WHERE rn = 1 - orders表缺
user_id索引?那EXISTS也会全表扫描,DISTINCT只是把这个问题延迟暴露

















