每日新增用户指用户首次出现的日期所对应的用户数,即每个用户仅在其最早行为日被计为新增,后续行为不重复计入;需用MIN()窗口函数按user_id标记首现日,再按该日去重计数。

什么是“每日新增用户”的正确定义
新增用户不是当天注册数,而是首次出现的用户。比如用户A在1月1日注册,1月3日又登录一次,他只算作1月1日的新增——后续行为不重复计入。窗口函数本身不生成新行,所以必须先标记每个用户的首次行为时间,再按天聚合。
用 MIN() + PARTITION BY user_id 标记首日
核心思路:对每个 user_id 计算其最早出现的日期(如 event_time 或 register_time),作为该用户的“新增日”。这一步必须用 MIN() 窗口函数,不能用 GROUP BY,否则会丢失原始行粒度,无法后续按天统计。
SELECT user_id, event_time::DATE AS event_date, MIN(event_time::DATE) OVER (PARTITION BY user_id) AS first_date FROM events;
-
event_time::DATE强制转日期,避免时间戳干扰分组 - 若源表无明确注册时间,用行为日志中的最早
event_time是常见替代方案 - PostgreSQL/MySQL 8.0+/Snowflake 都支持;SQLite 不支持窗口函数,需换写法
用 COUNT(DISTINCT) 统计每日新增数
上一步得到每行对应的 first_date 后,只需按该列 GROUP BY 并计数。注意必须用 COUNT(DISTINCT user_id),因为一个用户可能在首日有多条记录(如注册+完善资料),不能简单 COUNT(*)。
SELECT
first_date AS day,
COUNT(DISTINCT user_id) AS new_users
FROM (
SELECT
user_id,
MIN(event_time::DATE) OVER (PARTITION BY user_id) AS first_date
FROM events
) t
GROUP BY first_date
ORDER BY first_date;
- 如果想补全无新增的日期(如显示连续7天含0值),需额外
LEFT JOIN日期维表 - 性能敏感时,建议在
user_id和event_time上建复合索引 - BigQuery 中可改用
ARRAY_AGG(user_id ORDER BY event_time LIMIT 1)替代窗口函数,但语义等价性需验证
容易被忽略的边界情况
真实数据里,“新增”常被业务定义悄悄修改:是否排除测试账号?是否过滤机器人UA?是否要求完成手机号验证才算?这些逻辑必须在子查询中前置过滤,而不是加在窗口函数之后——否则会污染 MIN() 结果。
- 未过滤脏数据:某运营账号一天刷1000次注册,
MIN()仍把它当真实用户 - 时间字段为空:
MIN(NULL)返回 NULL,该用户会被漏掉,需加WHERE event_time IS NOT NULL - 跨时区场景:服务器时间 vs 用户本地时间,
event_time::DATE可能切错天,应统一转为 UTC 再截日期

















