
COUNT DISTINCT 是 SQL 中最常用但也最容易引发性能问题的聚合操作之一。它表面简单——统计某列中不重复值的个数,但底层执行时往往需要构建哈希表、排序或临时表,尤其在大数据量、高基数(distinct 值非常多)或缺乏索引的场景下,性能可能断崖式下降。
为什么 COUNT DISTINCT 慢?关键瓶颈在哪
数据库执行 COUNT(DISTINCT col) 通常要完成三步:扫描全表/索引 → 提取所有非 NULL 值 → 去重(用哈希或排序)→ 计数。这带来几个典型瓶颈:
- 内存压力大:高基数列(如用户ID、订单号)导致哈希表膨胀,可能触发磁盘临时表,I/O 成倍增加
- 无法有效利用索引:即使有索引,大多数引擎仍需读取全部索引键做去重,不能像普通 COUNT(*) 那样走索引元数据
- 无法并行优化:部分旧版本 MySQL 或某些分布式查询引擎对 DISTINCT 去重阶段并行支持弱
- 与 GROUP BY 组合时更重:例如
COUNT(DISTINCT user_id) GROUP BY day,会为每个分组单独去重,开销叠加
实用替代方案:按场景选更快的写法
不是所有“去重计数”都必须用 COUNT DISTINCT。根据业务精度和数据特征,可考虑这些优化路径:
-
用近似函数代替精确统计:PostgreSQL 用
APPROX_COUNT_DISTINCT(col),ClickHouse 用uniq(col),Spark SQL 用approx_count_distinct(col)。误差通常 -
预计算 + 汇总表:对高频查询维度(如每日 UV),用定时任务提前算好写入宽表,查询直接
SELECT uv FROM daily_summary WHERE dt = '2024-06-01' -
改用 EXISTS + 子查询(小范围适用):当去重列有强筛选条件(如最近7天活跃用户),可先过滤再关联去重,避免全表扫描:
SELECT COUNT(*) FROM (SELECT DISTINCT user_id FROM log WHERE dt >= '2024-06-01' AND event = 'login') t; -
组合索引覆盖 + 强制索引提示:MySQL 中若常查
COUNT(DISTINCT status, type),建联合索引(status, type)并用USE INDEX引导走索引扫描,减少回表
不同数据库的实际表现差异
各主流引擎对 COUNT DISTINCT 的实现策略不同,直接影响调优方向:
-
MySQL 8.0+:引入哈希聚合(Hash Aggregate),比老版本排序聚合快;但依然不支持索引跳过去重,且
innodb_buffer_pool_size过小会导致频繁落盘 -
PostgreSQL:默认用 HashAgg,配合
work_mem调大可显著提升性能;对GROUP BY + COUNT(DISTINCT)支持 HashAggregate 多阶段优化 -
ClickHouse:原生高效,
uniq()基于 HyperLogLog++,毫秒级响应亿级去重;uniqExact()才是精确版,慎用 -
Spark SQL / Presto:建议开启
spark.sql.adaptive.enabled=true,自动将大 DISTINCT 拆分为 Map-Side Partial + Reduce-Side Final,减少 shuffle 数据量
诊断是否真卡在 COUNT DISTINCT 上
别一慢就怪 COUNT DISTINCT。先确认瓶颈位置:
- 用
EXPLAIN ANALYZE(PG/MySQL 8.0+)看执行计划里是否有HashAggregate或Sort + Unique,并观察实际耗时占比 - 对比执行
COUNT(*)和COUNT(DISTINCT col)耗时:若后者慢 5 倍以上,基本可锁定去重开销为主因 - 检查该列的 distinct ratio:
SELECT COUNT(DISTINCT col)/COUNT(*) FROM tbl;若接近 1(如 UUID 列),说明高基数,哈希成本极高 - 监控临时表空间使用(如 MySQL 的
Created_tmp_disk_tables)、内存溢出日志
不复杂但容易忽略:多数慢查询不是语法写错,而是没意识到去重本身是个昂贵操作。把 COUNT DISTINCT 当成“隐式全量加载+内存去重”,就能更理性地设计索引、预计算或接受合理近似。











