group by本身不处理时间,必须配合date_format(reg_time, '%y-%m')或year(reg_time)*100+month(reg_time)提取年月再分组;否则按完整时间戳分组将导致每行独立,无业务意义。

用 DATE_FORMAT 或 YEAR_MONTH 提取年月做分组
MySQL 里最直接的方式是把注册时间转成「年-月」字符串或整数,再 GROUP BY。别用 MONTH(reg_time) 单独取月份——它会把不同年份的 1 月全混在一起,导致累计逻辑错乱。
推荐用 DATE_FORMAT(reg_time, '%Y-%m'),结果是 '2024-01' 这种可排序字符串;或者用 YEAR(reg_time) * 100 + MONTH(reg_time) 得到 202401 这种整数,两者都支持自然顺序排列,对后续累计计算很关键。
累计求和必须依赖窗口函数 OVER(ORDER BY ...)
单纯 GROUP BY 只能算当月新增,要累计总人数就得靠窗口函数。核心是:先按月聚合出每月新增数,再在这个结果集上用 SUM(new_users) OVER (ORDER BY ym) 累加。
注意点:
- ORDER BY 的字段必须和分组键一致(比如
ym),且不能是表达式别名未在 SELECT 中显式出现(某些 MySQL 版本会报错) - 如果用
DATE_FORMAT,ORDER BY 要写DATE_FORMAT(reg_time, '%Y-%m')或提前用子查询/CTE 把它拎出来 - 不支持窗口函数的老版本 MySQL(
完整可跑示例(MySQL 8.0+)
SELECT
ym,
monthly_new,
SUM(monthly_new) OVER (ORDER BY ym) AS cumulative_users
FROM (
SELECT
DATE_FORMAT(reg_time, '%Y-%m') AS ym,
COUNT(*) AS monthly_new
FROM users
WHERE reg_time >= '2023-01-01'
GROUP BY DATE_FORMAT(reg_time, '%Y-%m')
) t;
这个结果里 ym 是字符串,但按字典序排和时间序一致;cumulative_users 就是截至该月的总用户数。如果某月没数据(比如 2023-02 没人注册),那一行不会出现——需要补零得额外处理,比如用日历表 LEFT JOIN。
容易被忽略的边界:注册时间精度和时区
如果 reg_time 是 DATETIME 且带时区(如 TIMESTAMP 存的是 UTC),而业务统计按本地时间算,直接 DATE_FORMAT(reg_time, '%Y-%m') 会错。得先转换:DATE_FORMAT(CONVERT_TZ(reg_time, '+00:00', '+08:00'), '%Y-%m')。
另一个坑是:有些系统用 INT 存时间戳,得先转成日期:DATE_FORMAT(FROM_UNIXTIME(created_at), '%Y-%m'),否则 DATE_FORMAT(created_at, '%Y-%m') 会把数字当字符串切,结果全是 '0000-00'。
累计逻辑本身不难,真正卡住人的往往是时间字段的类型、精度、时区这三个点。










