优化 count(distinct) 的核心是减少去重数据量、避免全表扫描、利用索引与物化,并依场景选用近似算法、分层聚合、group by 替代、预计算等策略。

COUNT(DISTINCT ...) 是 SQL 中常用但容易成为性能瓶颈的操作,尤其在大数据量、高并发或分布式环境下。优化核心在于:减少参与去重的数据量、避免全表扫描、利用索引与物化中间结果,并根据场景选择更合适的替代方案。
用近似去重函数替代精确 COUNT(DISTINCT)
当业务允许少量误差(如报表、监控、AB测试),可直接使用近似算法,大幅降低资源消耗:
- PostgreSQL:用
APPROX_COUNT_DISTINCT()(需启用pg_stat_statements或扩展)或hyperloglog_add()+hyperloglog_size() - ClickHouse:原生支持
uniq()(HyperLogLog)、uniqCombined()(内存自适应)、uniqHLL12()(固定精度) - Spark SQL / Presto / Trino:支持
approx_count_distinct(),误差通常控制在 2% 以内 - MySQL 8.0+:暂无内置近似函数,但可通过采样 + 比例推算粗略估算(慎用于关键指标)
提前过滤、分层聚合,减少输入基数
不要对原始大表直接 COUNT(DISTINCT user_id),而是先缩小数据范围再统计:
- 加 WHERE 条件过滤无效数据(如 status = 'active'、dt >= '2024-01-01'),越早过滤,后续去重成本越低
- 对多维分析场景,先按时间/地域等高频维度 GROUP BY,再对每个分组内去重,最后 SUM 聚合(即“分治法”)
- 示例:
SELECT dt, COUNT(DISTINCT user_id) FROM log WHERE dt BETWEEN '2024-01-01' AND '2024-01-31' GROUP BY dt;
比全月一次性去重快数倍,且利于分区裁剪和并行执行
用 GROUP BY + COUNT(*) 替代 COUNT(DISTINCT)
本质是把“去重后计数”拆成两步:先去重,再计数。虽然逻辑等价,但执行引擎更容易优化,尤其配合索引时:
- 写法转换:
SELECT COUNT(DISTINCT user_id) FROM orders WHERE status = 'paid';
→ 改为:SELECT COUNT(*) FROM (SELECT DISTINCT user_id FROM orders WHERE status = 'paid') t; - 优势:子查询可走
(status, user_id)联合索引,避免排序/哈希去重的内存开销;部分数据库(如 MySQL 5.7+、Oracle)对这种写法有专门优化路径 - 注意:若 DISTINCT 结果集极大(如千万级唯一值),仍可能触发临时表或磁盘排序,此时需结合 LIMIT 或采样
物化中间结果或预计算汇总表
对查询频次高、更新不频繁的指标(如日活、周活),优先用预聚合代替实时计算:
- 每日凌晨跑批,将
DISTINCT user_id按天/设备类型/渠道等维度存入汇总表,查询直接 SELECT COUNT(*) - 使用物化视图(PostgreSQL 9.3+、ClickHouse、Oracle)自动维护去重后基础表,查询透明加速
- 流式场景可用 Kafka + Flink 做实时 HyperLogLog 状态聚合,对外提供毫秒级近似 UV 查询
- 小技巧:对用户 ID 类字段,可先 HASH 取模分桶(如 MOD(HASH(user_id), 100)),按桶分别去重再合并,缓解单点压力
不复杂但容易忽略:真正拖慢 COUNT(DISTINCT) 的往往不是算法本身,而是没做分区裁剪、缺少合适索引、或在 JOIN 后再去重。先看执行计划,定位是 I/O 瓶颈还是 CPU/MEM 瓶颈,再选对应策略。










