“每日新增用户”指每个用户在全表中最早出现日期落在当天的人数,需通过两层查询先按user_id取MIN(created_at),再按日期聚合,而非直接GROUP BY DATE(created_at)。

什么是“每日新增用户”?先别急着写 GROUP BY
每日新增用户不是「当天注册的用户数」,而是「每个用户在全库中最早出现的日期」落在当天的人数。直接 GROUP BY DATE(created_at) 会把老用户补资料、重登录等行为误算成新增——这是最常踩的坑。
核心判断标准只有一个:该用户在整张表里,MIN(created_at) 是不是这一天。
- 错误做法:
SELECT DATE(created_at), COUNT(*) FROM users GROUP BY DATE(created_at)→ 统计的是“当日注册记录数”,不是“首次出现用户数” - 正确逻辑链:每个用户 → 找出其最早注册时间 → 归到对应日期 → 按该日期聚合人数
- 如果表里有重复注册(比如用不同手机号)、测试账号、或字段未严格校验(如
created_at可为空或为未来时间),结果会偏高
MySQL 实现:两层嵌套子查询是最稳写法
不依赖窗口函数、不依赖变量、兼容 MySQL 5.7+,适合线上环境:
SELECT DATE(first_seen) AS `date`, COUNT(*) AS new_users
FROM (
SELECT user_id, MIN(created_at) AS first_seen
FROM users
WHERE created_at IS NOT NULL
AND created_at <= NOW()
GROUP BY user_id
) t
GROUP BY DATE(first_seen)
ORDER BY `date`;-
WHERE created_at IS NOT NULL必加——避免 NULL 干扰MIN() -
AND created_at <= NOW()防止脏数据含未来时间戳导致统计异常 - 外层
DATE(first_seen)比DATE_FORMAT(first_seen, '%Y-%m-%d')更轻量,且排序天然正确 - 如果要限定最近 30 天,加
HAVING DATE(first_seen) >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)在外层,而不是在内层 WHERE ——否则会漏掉“首次时间在 30 天前,但用户只在近期才被录入”的情况
图表展示需要补零?别硬拼 UNION ALL
折线图要求连续日期(比如 2026-08-01 到 2026-08-07),但某天没新增用户时原 SQL 不返回该行。传统用变量生成日期序列的方式(如 @num:=@num+1)强耦合表行数,不可靠。
更健壮的做法是建一张最小化的 dates 辅助表(只需 1000 行,覆盖 3 年):
CREATE TABLE dates (day DATE PRIMARY KEY);
INSERT INTO dates VALUES ('2026-08-01'), ('2026-08-02'), ... ; -- 或用循环插入然后 LEFT JOIN:
SELECT d.day AS `date`, COALESCE(t.new_users, 0) AS new_users
FROM dates d
LEFT JOIN (
SELECT DATE(first_seen) AS `date`, COUNT(*) AS new_users
FROM (
SELECT user_id, MIN(created_at) AS first_seen
FROM users
WHERE created_at IS NOT NULL AND created_at <= NOW()
GROUP BY user_id
) t1
GROUP BY DATE(first_seen)
) t ON d.day = t.`date`
WHERE d.day BETWEEN '2026-08-01' AND '2026-08-07'
ORDER BY d.day;- 辅助表可复用,比每次拼接变量安全得多
-
COALESCE(t.new_users, 0)确保空日期返回 0 而非 NULL - WHERE 放在外层控制日期范围,避免 JOIN 过多无用行
性能差?检查这三处索引和数据质量
当用户表超 50 万行后,上述查询可能变慢。瓶颈通常不在 SQL 写法,而在:
-
users(user_id, created_at)复合索引缺失 →GROUP BY user_id无法走索引,全表扫描 -
created_at字段未加索引 → 内层 WHERE 过滤慢;若经常按时间范围查,建议单独建索引 - 存在大量
user_id为空或重复的脏数据 → 先清理再统计,否则GROUP BY产出无效分组 - 注意:不要在
MIN(created_at)上建函数索引(MySQL 8.0+ 才支持),优先保证基础字段索引到位
真正难的不是写出那几行 SQL,而是确认 created_at 是否真代表“用户首次进入系统的时间”——如果是后台补录、迁移数据或埋点延迟,统计结果再准也没业务意义。


















