count(distinct) 并非单一聚合操作,而是先去重再计数,需维护哈希表或排序缓冲区,导致内存与性能开销远高于普通聚合,且自动忽略null。

DISTINCT 本身不是聚合操作,但和聚合函数一起用时会触发双重开销
很多人以为 COUNT(DISTINCT user_id) 是“一种聚合”,其实它内部要先做去重(类似 SELECT DISTINCT user_id),再计数。数据库必须为每个分组维护一个哈希表或排序缓冲区来去重,而普通聚合如 COUNT(*) 或 SUM(amount) 只需单次扫描、累加即可。
常见错误现象:
- 执行计划里频繁出现
Using temporary和Using filesort - 内存占用陡增,甚至触发磁盘临时文件(
Temp table on disk) - 同样数据量下,
COUNT(DISTINCT)比COUNT(*)慢 3–10 倍(尤其高基数字段如user_id)
关键原因有两点:
-
DISTINCT是行级去重逻辑,不管有没有GROUP BY,只要字段值不唯一,就得逐个比对或哈希 - 聚合函数本身不解决重复问题,
COUNT(DISTINCT)是把去重 + 计数两个步骤绑在一起执行,无法拆解优化
为什么 GROUP BY + 子查询有时比 COUNT(DISTINCT) 更快?
比如统计“每个部门的独立用户数”,写成:
SELECT dept_id, COUNT(*) FROM (SELECT DISTINCT dept_id, user_id FROM t) t1 GROUP BY dept_id
看起来绕,但它可能更快——因为子查询的 DISTINCT 可以走覆盖索引(如 INDEX(dept_id, user_id)),避免全表扫描;外层 GROUP BY 则只在几百/几千行的小结果集上运行。
而直接写:
SELECT dept_id, COUNT(DISTINCT user_id) FROM t GROUP BY dept_id
会让数据库对全表每行都尝试插入哈希表(user_id + dept_id 组合),内存压力大,且无法跳过无关分区或索引前导列。
使用场景判断要点:
- 字段基数低(如
status只有 3–5 个值)→COUNT(DISTINCT)开销可控 - 字段基数高(如
user_id上千万)→ 优先考虑GROUP BY+ 索引驱动的子查询 - 已有联合索引且顺序匹配(如查
dept_id, user_id,索引是(dept_id, user_id))→ 子查询能type: index全索引扫描,极快
DISTINCT 在聚合中对 NULL 的处理容易被忽略
COUNT(DISTINCT user_id) 会自动忽略 NULL 值——这不是 bug,是标准行为。但如果你误以为它和 COUNT(*) 一样“统计所有行”,就会漏掉 NULL 占比高的情况。
例如:
- 表中有 100 万行,其中 20 万行
user_id IS NULL -
COUNT(*)= 1000000,COUNT(DISTINCT user_id)最多只算 80 万(还可能更少,因重复) - 若业务定义“未登录用户也算独立访客”,那这个
COUNT(DISTINCT)就错了
此时应改用:
COUNT(DISTINCT COALESCE(user_id, CONCAT('null_', rand())))
或更稳妥地:先用 UNION ALL 把 NULL 显式转成占位符,再 COUNT(DISTINCT)。
别让 DISTINCT 扛全量数据,先过滤再计算
最常被忽视的性能点:DISTINCT 作用的数据集越大,哈希/排序开销越非线性增长。
错误写法:
SELECT COUNT(DISTINCT user_id) FROM events WHERE dt >= '2024-01-01'
正确做法(假设已按 dt 分区):
- 确认分区裁剪生效(
EXPLAIN看partitions是否只扫目标分区) - 加
AND user_id IS NOT NULL,减少哈希表 entry 数量 - 若
user_id有索引,可建覆盖索引INDEX(dt, user_id),避免回表
高基数场景下,哪怕只是减少 10% 的输入行数,COUNT(DISTINCT) 的内存峰值也可能下降 40% 以上——因为哈希冲突概率随数据量平方级上升。











