用DATE_FORMAT(created_at, '%Y-%m')提取年月字符串并GROUP BY可统计每月新增用户;勿用FORMAT()(仅适用于数字);created_at需为DATETIME/TIMESTAMP,时间戳需先FROM_UNIXTIME()转换;可加WHERE限制近12个月。

MySQL里用DATE_FORMAT按年月分组统计新增用户
直接用DATE_FORMAT(created_at, '%Y-%m')就能提取“年-月”字符串,再配合GROUP BY即可统计每月新增用户数。注意别用FORMAT()——那是用来格式化数字的,对日期无效,强行用会返回NULL或报错。
常见错误是写成FORMAT(created_at, '%Y-%m'),MySQL会提示Incorrect parameter count in the call to native function 'FORMAT',因为FORMAT只接受三个参数(数值、小数位、区域),不支持日期。
-
created_at字段类型必须是DATETIME或TIMESTAMP,如果是INT存的时间戳,得先转成日期:FROM_UNIXTIME(created_at) - 如果只要近12个月数据,加
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 12 MONTH),避免全表扫描 - 结果里的月份是字符串,排序时按字典序没问题(如
'2023-01''2023-12'),但别拿它做日期计算
PostgreSQL中用TO_CHAR替代DATE_FORMAT
PostgreSQL没有DATE_FORMAT,对应的是TO_CHAR(created_at, 'YYYY-MM')。大小写敏感,'yyyy-mm'会返回小写年月,影响分组一致性;'YY-MM'则只取两位年份,跨世纪时出错。
示例语句:
SELECT TO_CHAR(created_at, 'YYYY-MM') AS month, COUNT(*) AS new_users FROM users GROUP BY TO_CHAR(created_at, 'YYYY-MM') ORDER BY month;
- 如果
created_at带有时区(如TIMESTAMPTZ),TO_CHAR默认按数据库时区转换,要统一时区建议先用AT TIME ZONE 'UTC'归一化 - 索引无法直接加速
TO_CHAR表达式,如需高频查询,可建函数索引:CREATE INDEX idx_users_month ON users (TO_CHAR(created_at, 'YYYY-MM'))
SQL Server用FORMAT函数要注意性能和兼容性
SQL Server的FORMAT确实能处理日期,比如FORMAT(created_at, 'yyyy-MM'),但它是个高开销函数:每行都触发CLR调用,大数据量下比YEAR(created_at)*100 + MONTH(created_at)慢3–5倍。
更稳妥的做法是组合YEAR和MONTH:
SELECT YEAR(created_at) * 100 + MONTH(created_at) AS ym, COUNT(*) AS new_users FROM users GROUP BY YEAR(created_at), MONTH(created_at) ORDER BY ym;
-
FORMAT在SQL Server 2012+可用,但Azure SQL Database默认启用,而某些旧版兼容模式可能禁用 - 返回值是
nvarchar,如果后续要和其他整数字段关联,隐式转换可能引发意外执行计划 - 如果必须用
FORMAT(比如要本地化月份名),记得加OPTION (RECOMPILE)避免参数嗅探问题
通用陷阱:去重逻辑和时间边界容易被忽略
“新增用户”不是简单COUNT(*),而是首次出现的用户。如果表里有重复注册或测试账号,得先按用户ID取最早记录。
- 典型错误写法:
SELECT DATE_FORMAT(created_at, '%Y-%m'), COUNT(DISTINCT user_id)——这统计的是“当月活跃的去重用户”,不是“当月首次注册用户” - 正确做法:先用窗口函数找出每个
user_id的首次注册时间,再按月分组:
SELECT DATE_FORMAT(first_seen, '%Y-%m') AS month, COUNT(*) AS new_users FROM ( SELECT user_id, MIN(created_at) AS first_seen FROM users GROUP BY user_id ) t GROUP BY DATE_FORMAT(first_seen, '%Y-%m');
created_at是精确到秒的,但“某月新增”通常指自然月(如2023-01-01 00:00:00 至 2023-01-31 23:59:59)。用DATE_FORMAT或TO_CHAR天然满足,但用BETWEEN手动拼范围时容易漏掉最后一秒真正卡住人的往往不是函数怎么写,而是没想清楚“新增”的定义——是按注册时间?激活时间?还是第一次产生行为的时间?这个业务口径一旦定错,后面所有分组都白算。

















