count(distinct)性能差的根本原因是其执行模型强制单次扫描中同步完成去重与计数,需实时维护哈希表、内存与cpu开销大;而子查询+count(*)可先利用索引轻量去重再计数,更易走覆盖索引、内存占用低、并行友好。

COUNT(DISTINCT) 不是“写法问题”,而是执行模型决定它天生吃资源。InnoDB 必须在单次扫描中边读行、边维护哈希表(或排序结构)去重,内存占用高、CPU密集、无法有效利用索引覆盖,还常被优化器判定为“不值得走索引”——哪怕你建了索引,COUNT(DISTINCT user_id) 仍可能全表扫。
为什么 COUNT(DISTINCT) 比子查询 + COUNT(*) 还慢
根本差异在执行路径:
-
COUNT(DISTINCT col):一次扫描,同时做“去重 + 计数”,必须为每个值维护唯一性状态,哈希表膨胀快,易触发磁盘临时表 -
SELECT COUNT(*) FROM (SELECT DISTINCT col FROM t WHERE ...):先轻量去重(可走索引、可提前过滤),再对小结果集计数;只要子查询输出行数远小于原表,外层COUNT(*)几乎无压力
关键不是“语法好看”,是数据库能否把“去重”和“计数”拆开并分别优化。
子查询写法真正生效的三个硬条件
不是所有 SELECT COUNT(*) FROM (SELECT DISTINCT ...) 都快,它只在满足以下全部条件时才明显提速:
-
WHERE条件选择率高(例如dt >= '2026-08-01'),能筛掉 90%+ 行数 - 去重字段
col有单列索引,或作为联合索引最左前缀(例如CREATE INDEX idx_dt_uid ON log (dt, user_id)) - 子查询里只选去重字段,不带其他非索引列(
SELECT DISTINCT user_id✅,SELECT DISTINCT user_id, name❌——后者会强制回表)
MySQL 中容易被忽略的索引陷阱
即使你给 user_id 加了索引,COUNT(DISTINCT user_id) 仍可能不走索引,原因包括:
- 优化器发现需判断
NULL值(DISTINCT默认忽略NULL,但判断本身要额外逻辑) - 统计过程中需要访问聚簇索引获取隐藏字段(比如事务版本信息),导致无法纯索引扫描
- 而子查询方式可以绕过:只要
SELECT DISTINCT user_id FROM log WHERE dt = '2026-08-10'能走idx_dt_uid,就全程免回表
真正卡住性能的,往往不是“有没有索引”,而是“去重动作是否被迫和全表扫描绑死”。子查询不是银弹,但它把控制权交还给了索引设计和 WHERE 条件质量——这两点,才是你能实际动手改的地方。











