mysql按月分组应使用date_format(created_at, '%y-%m'),postgresql用to_char(created_at, 'yyyy-mm');避免分开提取年月,注意时区转换(如utc转东八区)和空月份应用层补全。

MySQL里用DATE_FORMAT按月分组的典型写法
直接用 DATE_FORMAT(created_at, '%Y-%m') 是最常见也最稳妥的方式,它把日期转成“2024-03”这样的字符串,天然可分组、可排序、可读性强。
注意别用 YEAR(created_at) 和 MONTH(created_at) 分开取再拼接——看似等价,但分组时会变成 (2024, 3) 这样的元组,在某些旧版本 MySQL 或 ORM 中可能隐式转成字符串后顺序错乱(比如 2024-10 排在 2024-2 前面)。
-
GROUP BY DATE_FORMAT(created_at, '%Y-%m')是安全底线,别省略引号里的格式 - 如果字段是
DATETIME或TIMESTAMP,DATE_FORMAT仍能正确截断到日,不影响按月逻辑 - 避免用
STR_TO_DATE(DATE_FORMAT(...), ...)套娃转换——没意义,还拖慢查询
PostgreSQL 怎么对应实现:用TO_CHAR而不是EXTRACT
PostgreSQL 没有 DATE_FORMAT,但 TO_CHAR(created_at, 'YYYY-MM') 效果一致。千万别用 EXTRACT(YEAR FROM created_at) 和 EXTRACT(MONTH FROM created_at) 分开取——它们返回的是数字,分组时 (2024, 3) 和 (2024, 10) 无法自然排序,导出后还得二次处理。
-
TO_CHAR(created_at, 'YYYY-MM')输出字符串,排序和分组都按字典序来,结果直观 - 格式串里必须大写
YYYY和MM,小写yy或mm会出错或截断 - 如果表数据量大,记得在
created_at上建索引;TO_CHAR本身无法走索引,但过滤条件如created_at >= '2024-01-01'仍能用上
统计结果为空月份怎么补全:别硬写 LEFT JOIN 生成日历表
业务上常需要“不管有没有数据,2024年1–12月都列出来”,这时候最容易掉坑里:有人试图用 LEFT JOIN 去连一个手动生成的月份表,结果发现跨年、边界、时区全乱套。
- 简单场景(比如就查最近12个月),用应用层补空更可控:先查出有数据的月份,再用 Python/JS 补全缺失的
'2024-01'到'2024-12' - 真要 SQL 层补全,推荐用递归 CTE(MySQL 8.0+/PostgreSQL),但注意深度限制和性能——12个月安全,120个月就卡了
- 别在
WHERE里写created_at IS NOT NULL后再想补空,逻辑矛盾:NULL 已被过滤,补也补不回来
时区问题导致统计偏差:created_at 存的是 UTC,但你要按本地月统计
很多系统数据库存的是 UTC 时间,但运营要看“北京时间当月”。直接用 DATE_FORMAT(created_at, '%Y-%m') 会按 UTC 时间算——比如北京时间 2024-03-01 00:00:00 是 UTC 的 2024-02-29 16:00:00,结果被算进 2 月。
- MySQL:改用
DATE_FORMAT(CONVERT_TZ(created_at, '+00:00', '+08:00'), '%Y-%m'),确保时区转换后再格式化 - PostgreSQL:用
TO_CHAR(created_at AT TIME ZONE 'Asia/Shanghai', 'YYYY-MM') - 检查
created_at字段实际存的是什么时区——有些老系统存的是本地时间却没标注,这种比 UTC 还难搞,得先确认数据源头
真正麻烦的不是函数怎么写,而是搞清“月”这个单位到底以谁为准:数据库服务器时区?应用配置时区?还是业务合同里白纸黑字写的“东八区”?这三个地方对不上,统计结果就永远差那么一两天。










