COUNT DISTINCT在大数据量下慢,因其需先建哈希表或排序去重,内存不足时落盘导致高IO和CPU开销;优化应改用子查询GROUP BY预聚合,并建立dt与user_id联合索引。

COUNT DISTINCT 在大数据量下为什么慢
因为 COUNT(DISTINCT ...) 必须先去重再计数,数据库通常要建临时哈希表或排序,内存吃紧时还会落盘,IO 和 CPU 开销都大。尤其当去重字段基数高(比如用户ID)、且没索引时,执行计划里常看到 Hash Aggregate 或 Sort 节点占主导。
用子查询预聚合替代 COUNT DISTINCT 的典型写法
核心思路是把“去重+计数”拆成两步:先在子查询里用 GROUP BY 去重,再在外层统计行数。这样能复用索引、减少中间结果集大小,也更容易被优化器下推条件。
- 原始低效写法:
SELECT COUNT(DISTINCT user_id) FROM events WHERE dt = '2024-06-01'; - 优化后写法:
SELECT COUNT(*) FROM (SELECT DISTINCT user_id FROM events WHERE dt = '2024-06-01') t; - 更进一步(推荐):
SELECT COUNT(*) FROM (SELECT user_id FROM events WHERE dt = '2024-06-01' GROUP BY user_id) t;——GROUP BY比DISTINCT更可控,便于加LIMIT或关联其他字段
MySQL 8.0+ 与 PostgreSQL 的特殊处理
MySQL 8.0+ 对 COUNT(DISTINCT) 做了哈希优化,但仅限单列且有合适索引;PostgreSQL 则对 GROUP BY 子查询的物化更积极,有时自动选择 HashAggregate 而非 SortAggregate。两者都依赖统计信息准确——如果 ANALYZE events 没跑过,优化器可能误判,反而选错执行路径。
- 检查执行计划必须用
EXPLAIN ANALYZE(PostgreSQL)或EXPLAIN FORMAT=TREE(MySQL 8.0+) - 确保
dt和user_id有联合索引:CREATE INDEX idx_dt_uid ON events(dt, user_id); - 若只查最近 N 天,别用
WHERE dt >= ...配范围扫描,而用IN枚举具体日期,避免索引失效
当去重维度多于一列时怎么写才不翻车
COUNT(DISTINCT a, b) 看似简洁,但多数引擎(MySQL 5.7/8.0、PostgreSQL)并不真正支持多列哈希去重,实际会拼接字符串或转为元组,隐式类型转换易出错,且无法利用索引。稳妥做法仍是子查询 + GROUP BY。
- 错误示范:
COUNT(DISTINCT user_id, event_type)—— 在 MySQL 中等价于COUNT(DISTINCT CONCAT(user_id, '_', event_type)),NULL处理诡异 - 正确写法:
SELECT COUNT(*) FROM (SELECT user_id, event_type FROM events GROUP BY user_id, event_type) t; - 注意:如果
event_type是文本且长度不定,建议先CAST(event_type AS VARCHAR(32))统一长度,避免哈希碰撞或排序不稳定
WHERE 条件,导致全表去重;或者外层 COUNT(*) 没加别名,在复杂嵌套中引发解析歧义。

















