正确做法是先按用户标识分组再用MIN():SELECT user_id, MIN(created_at) AS first_seen FROM user_events GROUP BY user_id;需确保user_id唯一且非空,再用子查询或CTE按“年-月”聚合统计新增人数。

怎么用 MIN() 找出每个用户的首次登录/注册时间
关键不是对全表直接 MIN(created_at),而是先按用户分组,再取每组最早时间。否则会得到整个表里最早的那一条记录,完全偏离目标。
常见错误是写成 SELECT MIN(created_at) FROM users —— 这只返回一个全局最小值,毫无意义。
正确做法是搭配 GROUP BY user_id(或 id、email 等唯一标识):
SELECT user_id, MIN(created_at) AS first_seen FROM user_events GROUP BY user_id;
- 确保
user_id是能稳定标识“一个人”的字段;用手机号或邮箱时要注意去重和空值 - 如果原始表没有明确的用户标识(比如只有日志流水),得先用
REGEXP或SUBSTRING提取设备 ID / 匿名 ID,再分组 -
MIN()对DATETIME和TIMESTAMP类型天然有效,但对字符串格式的日期(如'2024-03-15 10:22')也成立——前提是格式统一且可字典序比较
如何把首次日期转成“年-月”并统计每月新增人数
不能直接在 GROUP BY 里套 MIN() 再套 DATE_FORMAT(),得先算出每人首次时间,再按月聚合——必须用子查询或 CTE。
MySQL 示例(8.0+):
WITH first_login AS ( SELECT user_id, DATE_FORMAT(MIN(created_at), '%Y-%m') AS ym FROM user_events GROUP BY user_id ) SELECT ym, COUNT(*) AS new_users FROM first_login GROUP BY ym ORDER BY ym;
- PostgreSQL 要用
TO_CHAR(MIN(created_at), 'YYYY-MM'),SQLite 用strftime('%Y-%m', MIN(created_at)) - 注意时区:如果
created_at是 UTC,而业务要求按本地月份统计,得先用CONVERT_TZ()或AT TIME ZONE调整 - 如果某用户有多条同一天的记录,
MIN()不会重复计数,这点放心
为什么不能直接用注册表的 created_at,而要用行为日志?
因为“新增用户”本质是“首次产生有效行为”,不是“填了注册表单”。注册表可能被刷、被弃置,而点击、下单、完播等行为更能反映真实用户。
- 典型场景:APP 注册后从未打开,这类用户不该计入当月新增
- 如果只有注册表,且确认数据干净,那可以直接用:
SELECT DATE_FORMAT(created_at, '%Y-%m') AS ym, COUNT(*) FROM users GROUP BY ym - 但一旦要排除测试账号、机器人、无效邮箱,就得关联设备指纹、IP、行为序列做清洗——这时候就绕不开
MIN()+ 行为日志了
容易被忽略的边界情况
统计结果偏少,往往不是 SQL 写错,而是数据本身有陷阱。
-
NULL的user_id或created_at:GROUP BY会自动过滤掉,导致漏人;加WHERE user_id IS NOT NULL AND created_at IS NOT NULL显式控制 - 跨午夜的事件归因:比如用户 3 月 31 日 23:59 注册,但服务端日志落库延迟到 4 月 1 日 00:02,
MIN(created_at)会记到 4 月——得用event_time字段而非insert_time - 同一用户多设备:如果用设备 ID 分组而非用户 ID,会导致一人多计;反之,若用户注销重登没合并账号,又会少计
真正难的从来不是写对 MIN(),而是搞清“新增”在你业务里到底由哪个动作定义、对应哪张表、哪些字段可信。

















