mysql中yearweek()默认以周日为起点且忽略时区,易致utc时间与本地时间分组偏差;year()+month()跨年会混组,应改用group by year(), month();postgresql推荐date_trunc(),sql server需搭配year()防跨年错乱;null和非法时间戳会导致分组丢失,须显式处理。

MySQL里用YEARWEEK()和YEAR()+MONTH()分组要注意时区和边界
直接用GROUP BY YEARWEEK(created_at)能按周聚合,但默认按周日为一周起点,且不考虑时区——如果created_at是UTC时间而业务在东八区,周一00:00(北京时间)实际存的是周日16:00(UTC),就会被分到上一周。同理,YEAR(MONTH())看似简单,但MONTH(created_at)只返回1–12,跨年时2023-12和2024-01会因YEAR()缺失而混在一起。
- 安全做法是组合
YEAR()和MONTH():GROUP BY YEAR(created_at), MONTH(created_at) - 按周推荐
YEARWEEK(created_at, 1),第二个参数1表示周一为每周开始,避免周日偏差 - 若字段是
TIMESTAMP类型,确认服务器时区与业务一致,否则先用CONVERT_TZ()转换
PostgreSQL中DATE_TRUNC()是更干净的写法
MySQL靠拼函数,PostgreSQL原生支持DATE_TRUNC('week', created_at)和DATE_TRUNC('month', created_at),语义清晰、时区可控。它默认按当前timezone设置截断,不会出现MySQL里YEARWEEK()那种隐式周起始逻辑。
-
DATE_TRUNC('week', created_at)总是从周一00:00开始截断,不受lc_time影响 - 要强制按自然月(而非日历月),可用
DATE_TRUNC('month', created_at) + INTERVAL '1 month' - INTERVAL '1 day'拿到月末日期再分组 - 注意
DATE_TRUNC返回的是TIMESTAMP WITHOUT TIME ZONE,如果字段带时区,先用created_at AT TIME ZONE 'Asia/Shanghai'对齐
SQL Server用DATEPART()时别漏掉YEAR()防跨年错乱
GROUP BY DATEPART(WEEK, created_at)单独用会把不同年的同一周数(比如2023年第1周和2024年第1周)合并在一组,结果完全不可信。必须搭配YEAR()或DATEPART(YEAR, created_at)一起用。
- 正确写法:
GROUP BY DATEPART(YEAR, created_at), DATEPART(WEEK, created_at) - 周计算依赖
DATEFIRST设置,默认是7(周日),设成1(周一)需提前执行SET DATEFIRST 1 - 按月更稳妥:
GROUP BY DATEPART(YEAR, created_at), DATEPART(MONTH, created_at),比FORMAT(created_at, 'yyyy-MM')快且可索引
所有数据库通用陷阱:NULL值和空时间戳导致分组丢失
如果created_at允许为NULL,或者有非法时间如'0000-00-00'(MySQL宽松模式),这些记录会在GROUP BY后彻底消失,统计总数变少但无报错。这不是语法问题,是数据质量漏斗。
- 先查:
SELECT COUNT(*) FROM table WHERE created_at IS NULL OR created_at = '0000-00-00' - 分组时显式保留NULL:
GROUP BY COALESCE(DATE_TRUNC('month', created_at), 'unknown')(PostgreSQL)或GROUP BY IFNULL(YEAR(created_at), -1), IFNULL(MONTH(created_at), -1)(MySQL) - 生产环境建议加检查约束:
ALTER TABLE table ADD CHECK (created_at >= '2000-01-01')
时区、跨年、NULL这三块不手动兜底,跑出来的周/月统计看着整齐,实际每一条都可能偏移一天或跳过整月。










