COUNT(DISTINCT col)必须建临时去重集合,因其需在内存或磁盘中维护哈希表或排序结构,逐行判断并存储唯一值,无法跳过该过程;无索引、低选择性、多列组合或高基数场景下易触发全表扫描与落盘,导致性能断崖式下降。

为什么 COUNT(DISTINCT) 必须建临时去重集合
COUNT(DISTINCT col) 不是简单遍历加个计数器就能完成的。数据库必须在内存或磁盘中维护一个去重结构(如哈希表或排序后的唯一值列表),每遇到一个新值就判断是否已存在,再决定是否插入。这个过程无法跳过、无法并行化到单次扫描外——哪怕你只查一列,它也得把所有匹配行的 col 值全拉出来跑一遍去重逻辑。
常见错误现象:EXPLAIN 中看到 Using temporary; Using filesort(MySQL)或 HashAggregate(PostgreSQL)就说明正在干这事;如果数据量大且无索引,大概率触发全表扫描 + 内存溢出写磁盘,速度断崖式下跌。
COUNT(DISTINCT) 的性能瓶颈在哪几类场景最明显
不是所有 COUNT(DISTINCT) 都慢,但以下情况会显著放大开销:
- 去重字段选择性极低:比如
status只有'pending'、'done'、'failed'三个值,却对上亿行做COUNT(DISTINCT status)——数据库仍要逐行读、逐行哈希、逐行判重,浪费大量 I/O 和 CPU - 字段无索引且查询条件无法过滤掉大部分数据:例如
WHERE created_at > '2025-01-01'本可快速定位,但created_at没索引,引擎只能扫全表,再对每一行提取user_id去重 - 多列去重:
COUNT(DISTINCT user_id, product_id)要对组合值哈希,内存占用和碰撞概率都比单列高得多;SQLite 和 SQL Server 2014 及以前还不支持,直接报错Incorrect syntax near ',' - 嵌套子查询里反复用:
SELECT a, COUNT(DISTINCT b), COUNT(DISTINCT c) FROM t GROUP BY a在传统执行器中会触发两次独立去重,不是一次扫描搞定
什么时候该放弃 COUNT(DISTINCT) 改用近似统计
当你要同时算多个去重指标,或单次去重基数超过千万级且允许小误差时,硬扛 COUNT(DISTINCT) 就是自找麻烦。
可用方案包括:
- BigQuery / Spark SQL:
APPROX_COUNT_DISTINCT(<code>col),误差通常 - Trino / Presto:
approx_distinct(<code>col) - PostgreSQL:没有内置近似函数,但可用
#(SELECT array_agg(DISTINCT x) FROM t)?别试——这是错的,array_agg不去重,且会爆内存 - 真要精确又快:提前物化去重结果到汇总表,或用
MATERIALIZED VIEW(PostgreSQL 9.4+)定期刷新
容易被忽略的关键细节
COUNT(DISTINCT) 对 NULL 自动忽略,这点和 SUM、AVG 一致,但和 COUNT(*) 不同——后者会把 NULL 行也算进去。如果你的业务逻辑依赖 NULL 计入总数,就不能用 DISTINCT,得换思路,比如用 COUNT(*) - COUNT(col) 算 NULL 个数。
另外,COUNT(DISTINCT *) 是语法错误,DISTINCT 后面必须跟具体列或表达式,不能偷懒。

















