count(distinct user_id) 是统计去重用户数最直接高效的方式,需确保 user_id 唯一标识用户且非 null,主流数据库均支持该语法。

count(distinct user_id) 是最直接的写法
统计去重后的用户数,核心就是用 COUNT(DISTINCT ...)。它会先对字段做唯一值去重,再计数,一步到位。别试图先 GROUP BY 再套子查询,既慢又易错。
常见错误是写成 COUNT(*) 配合 GROUP BY user_id,结果得到的是每组一行,不是总数;或者漏掉 DISTINCT,直接算所有记录数。
- 必须确保
user_id是能标识用户的字段(比如不是name这种可能重复的) - 如果
user_id允许为NULL,DISTINCT会把所有NULL当作一个值计入——多数场景下你可能想排除它,可加WHERE user_id IS NOT NULL - MySQL 5.7+、PostgreSQL、SQL Server、Oracle 都支持该语法;SQLite 也支持,但旧版本(COUNT(DISTINCT ...) 中用表达式
遇到大数据量时性能明显变慢怎么办
COUNT(DISTINCT user_id) 在千万级以上表上容易变慢,因为数据库要构建哈希表或排序去重。不是语法写错了,是底层代价高。
优化方向不是换写法,而是看能不能绕开实时精确统计:
- 加复合索引:如
(status, user_id)(如果常带条件过滤),让扫描更高效 - 用近似统计函数:PostgreSQL 有
APPROX_COUNT_DISTINCT(user_id),BigQuery 用APPROX_COUNT_DISTINCT,误差可控且快得多 - 预计算:把每日去重用户数存到汇总表,查时直接
SELECT SUM(daily_uv) FROM summary_table - 避免在
WHERE条件里用函数处理user_id,比如WHERE UPPER(user_id) = 'ABC',会导致索引失效,连带拖慢COUNT(DISTINCT)
不同数据库对 NULL 和空字符串的处理差异
DISTINCT 对 NULL 的处理是 SQL 标准行为:所有 NULL 被视为相等,只算一次。但空字符串 '' 和 NULL 是不同值,会被分别计数。
这容易引发业务误解——比如用户注册时没填 ID,后端存了空字符串而非 NULL,结果统计时多算了一类“假用户”。
- MySQL 默认把空字符串和
NULL当作不同值 - PostgreSQL 同样区分
''和NULL - 检查数据质量:执行
SELECT COUNT(*) FROM t WHERE user_id = '' OR user_id IS NULL,确认异常值比例 - 清洗建议:入库前统一转
NULL,或业务层约定空值不参与 UV 统计
需要按时间维度分组统计去重用户数
比如“每天的独立用户数”,不能只写 COUNT(DISTINCT user_id),必须配合时间字段分组。
典型写法是:SELECT DATE(event_time) AS dt, COUNT(DISTINCT user_id) AS uv FROM events GROUP BY DATE(event_time)
- 注意时区:如果
event_time是TIMESTAMP WITH TIME ZONE,用DATE(event_time AT TIME ZONE 'UTC')显式指定,避免服务器本地时区干扰 - 避免用
FROM_UNIXTIME或TO_CHAR等函数包裹时间字段做分组——可能使索引失效 - 如果要跨天统计(如“最近7天UV”),别用
GROUP BY,改用窗口函数或子查询 +BETWEEN,否则逻辑不对
EXPLAIN 看执行计划,重点盯 rows 和 type 字段;线上大表别直接跑全量 COUNT(DISTINCT),先用 LIMIT 或采样验证逻辑。











