使用 SQL 统计不重复用户数时,有个写法几乎每天都能看到有人踩坑——COUNT DISTINCT。很多人想当然地写成 SELECT COUNT DISTINCT user_id FROM table,结果数据库直接报语法错误。先记住一个铁律:COUNT(DISTINCT column) 才是唯一正确写法,DISTINCT 必须作为 COUNT 的参数修饰符,紧贴括号内,且只作用于单个表达式。错误写法如 COUNT DISTINCT user_id 或 COUNT(user_id, DISTINCT) 一律无效。
DISTINCT 必须包裹在 COUNT 里面,不能写成 COUNT DISTINCT
正确写法是 COUNT(DISTINCT user_id)。常见错误现象:
SELECT COUNT DISTINCT user_id FROM events;→ 语法错误(如 PostgreSQL/MySQL 报syntax error at or near "DISTINCT")SELECT COUNT(user_id, DISTINCT)→ 无效函数调用
实操建议:
- 始终用
COUNT(DISTINCT column_name),哪怕只统计一列。 - 支持多列去重的数据库(如 PostgreSQL)允许
COUNT(DISTINCT user_id, event_type),但 MySQL 不支持,会报错。 - 如果列可能为
NULL,DISTINCT自动忽略NULL值,不参与计数。
WHERE 条件要放在 COUNT 外面,不能塞进括号里
COUNT(DISTINCT user_id) 只负责去重计数,过滤逻辑必须由 WHERE 或 HA VING 承担。有人误以为可以写成 COUNT(DISTINCT user_id WHERE event_time > '2024-01-01'),这是无效语法(除非用条件聚合,但那是另一回事)。
使用场景:统计「近7天活跃用户」
- ✅ 正确:
SELECT COUNT(DISTINCT user_id) FROM events WHERE event_time >= CURRENT_DATE - INTERVAL '7 days'; - ❌ 错误:
SELECT COUNT(DISTINCT user_id WHERE event_time >= ...)(仅 SQLite 支持这种写法,且非标准)
性能影响:加 WHERE 能大幅减少扫描行数,比先 COUNT(DISTINCT) 再过滤高效得多;索引(如 (event_time, user_id))能明显加速这类查询。
遇到大数据量时,COUNT(DISTINCT) 可能慢或内存溢出
当 user_id 去重后仍有千万级唯一值,COUNT(DISTINCT) 通常需要构建哈希表或排序,容易触发磁盘临时表(MySQL)或内存超限(PostgreSQL 的 work_mem 不足)。
可选应对方式:
- 用近似算法:PostgreSQL 可用
APPROX_COUNT_DISTINCT(user_id),ClickHouse 用uniq(user_id),误差率通常 < 1%。 - 分桶预计算:每天跑一个
INSERT INTO daily_active_users SELECT CURRENT_DATE, COUNT(DISTINCT user_id) FROM events WHERE event_date = CURRENT_DATE;,查时直接SUM(count)。 - 避免在大宽表上直接跑:先用
WHERE尽量缩小范围,再COUNT(DISTINCT)。
注意:不同数据库对 COUNT(DISTINCT) 的底层实现差异很大——MySQL 8.0+ 会自动尝试使用临时哈希表,而旧版只能排序;Spark SQL 默认走 MapReduce 阶段,shuffle 开销高。
GROUP BY 后用 COUNT(DISTINCT) 容易漏掉空组或 NULL 分组
比如按渠道统计活跃用户:SELECT channel, COUNT(DISTINCT user_id) FROM events GROUP BY channel;。如果某渠道数据全为 NULL,该渠道不会出现在结果中(因为 GROUP BY NULL 被合并成一组,但很多引擎直接跳过)。
容易踩的坑:
channel为NULL时,整行仍参与分组,但结果里可能看不到NULL行(取决于数据库默认行为,如 MySQL 5.7 默认显示,PostgreSQL 显示)。- 想补全所有渠道(包括零活跃的),得用维表
LEFT JOIN,不能只靠GROUP BY。 - 多个字段组合去重时,如
COUNT(DISTINCT user_id, session_id),MySQL 不支持,必须改写为子查询或用CONCAT拼接(但有长度和字符集风险)。
真实业务里,“活跃用户”定义常依赖事件类型(如只算 page_view 或 purchase),漏掉 WHERE event_type IN (...) 是最常被忽略的逻辑点。
