COUNT(DISTINCT)是标准SQL中唯一能原子性统计去重行数的方式,它自动忽略NULL值,而GROUP BY user_id不包含NULL分组,易导致统计偏差,且多列去重、索引利用和大数据性能等方面均存在本质差异。

COUNT(DISTINCT) 是唯一能直接、原子性地统计去重后行数的标准 SQL 方式,其他写法(如子查询套 COUNT(*))在语义或性能上都可能出问题。
为什么不能用 GROUP BY + COUNT(*) 替代 COUNT(DISTINCT)
常见误解是先 GROUP BY user_id 再对结果计数,但这需要两层嵌套,且容易漏掉 NULL 值处理逻辑。更关键的是:标准 SQL 中,COUNT(DISTINCT user_id) 会自动忽略 NULL;而 GROUP BY user_id 默认不包含 NULL 分组(不同数据库行为不一致),导致统计偏差。
- MySQL 8.0+ 和 PostgreSQL 对
NULL在DISTINCT中的处理一致:直接跳过 - 如果字段允许为
NULL且你希望把NULL当作一个独立“用户”,得显式补上:COUNT(DISTINCT user_id) + (CASE WHEN COUNT(*) > COUNT(user_id) THEN 1 ELSE 0 END) - 用
GROUP BY实现等价逻辑至少要三层:SELECT COUNT(*) FROM (SELECT DISTINCT user_id FROM t) AS _—— 多一次物化,执行计划更重
COUNT(DISTINCT) 在多列组合去重时的行为
当需要按多个字段联合去重(比如“同一用户在同一天的首次登录”),可直接写 COUNT(DISTINCT user_id, login_date),但注意:不是所有数据库都支持多列 DISTINCT。
- PostgreSQL、SQL Server(2017+)、MySQL 8.0+ 支持
COUNT(DISTINCT a, b) - SQLite 和旧版 MySQL(5.7 及以前)不支持,必须改写为:
COUNT(DISTINCT CONCAT(a, '|', b))(前提是字段不含|,否则会误合并) - Oracle 需用
COUNT(DISTINCT (a, b)),括号不能省 —— 少一个括号就报ORA-30483 - 大数据场景下(如 Hive/Spark SQL),多列
DISTINCT会触发 shuffle,比单列开销大得多,必要时考虑预聚合
性能瓶颈在哪?什么时候该避免用 COUNT(DISTINCT)
本质是数据库必须扫描全部匹配行、构建哈希表或排序去重,无法走索引直接得出结果(除非有覆盖索引且优化器足够智能)。
- 在亿级用户表中统计
COUNT(DISTINCT user_id),即使有user_id索引,仍大概率触发全表扫描或大内存哈希 - MySQL 的
tmp_table_size或 PostgreSQL 的work_mem不足时,会落盘临时文件,速度骤降 - 替代方案要看场景:实时性要求不高可用物化视图;高并发轻量查询可用 HyperLogLog 近似算法(如 Redis 的
PFCOUNT或 ClickHouse 的uniq()) - 千万别在 WHERE 条件里对
DISTINCT字段做函数操作,比如COUNT(DISTINCT UPPER(name))—— 索引完全失效,且大小写归一化应前置到 ETL 阶段
真正难的不是写对语法,而是判断「这个 DISTINCT 是否真的需要精确值」——很多报表里的“总人数”其实容忍 ±1% 误差,这时候用近似函数反而更稳。别让精确性成为性能瓶颈的遮羞布。










