MySQL WEEK() 默认 mode 0 以周日为始且首周须含1月1日,导致2024-01-01(周一)可能归为第52周;跨年周错乱、无法区分年份、不走索引是三大坑,应改用 YEARWEEK(date,1) 或日期偏移对齐周一。

week() 函数默认行为坑在哪
MySQL 的 WEEK() 函数不按「自然周」(周一到周日或周日到周六)算,而是按 MySQL 自己的模式算,默认用 mode 0:周日为一周开始,且第一周必须包含 1 月 1 日。结果就是 2024-01-01(周一)可能被算进第 52 周,而不是第 1 周。
常见错误现象:GROUP BY WEEK(created_at) 导致跨年周错乱,比如 2023-12-31 和 2024-01-01 被分到不同组,明明是同一自然周。
- 用
WEEK(date, mode)显式指定 mode 才可控,推荐mode = 1(周一为每周开始,且第一周至少有 4 天在当年) - 更稳妥的做法是用
YEARWEEK(date, 1),它把年份和周数打包成一个整数(如 202401),天然避免跨年歧义 - 注意
WEEK()返回的是纯数字(0–53),无法区分 2023 年第 52 周和 2024 年第 52 周
按自然周(周一至周日)分组的写法
要让每周从周一算起、且跨年不割裂,核心是组合 YEARWEEK() + STR_TO_DATE() 或直接用日期偏移对齐周一。
实操建议:
- 最简可靠方式:
GROUP BY YEARWEEK(created_at, 1),再用MIN(created_at)或STR_TO_DATE(CONCAT(YEARWEEK(created_at, 1), ' Monday'), '%X%V %W')推算当周周一日期 - 如果只要周起始日(周一)作为分组标签,用:
DATE_SUB(created_at, INTERVAL WEEKDAY(created_at) DAY),它把任意日期“拉回”到当周周一 - 别用
WEEKOFYEAR(),它等价于WEEK(date, 0),同样有跨年陷阱
时区和 datetime 类型的影响
created_at 是 DATETIME 还是 TIMESTAMP?这直接影响周计算结果,尤其在服务器时区和业务时区不一致时。
常见错误现象:数据库设 UTC,但业务按北京时间统计,导致凌晨数据被归入错误的一周。
- 确保传入
WEEK()或YEARWEEK()的值已是目标时区时间,例如:WEEK(CONVERT_TZ(created_at, '+00:00', '+08:00'), 1) - 如果字段是
TIMESTAMP,MySQL 存储时已转为 UTC,读取时会自动转回 session 时区 —— 但WEEK()函数内部不走这个转换,它直接按存储值算,所以务必显式转换 - 线上环境务必检查
SELECT @@time_zone, @@session.time_zone,别依赖默认值
性能要注意:week 函数不能走索引
WHERE WEEK(created_at) = 1 或 GROUP BY WEEK(created_at) 会让 MySQL 无法使用 created_at 上的索引,全表扫描风险高。
优化路径:
- 改用范围查询代替函数:比如查「2024 年第 1 周」,写成
WHERE created_at >= '2024-01-01' AND created_at - 如果高频按周聚合,考虑冗余一个
week_start_date字段(DATE类型),并建索引:ALTER TABLE orders ADD COLUMN week_start_date DATE;,用触发器或应用层维护 - 用
YEARWEEK(created_at, 1)虽比WEEK()稍好,但仍属函数计算,无法索引加速
真正麻烦的不是语法怎么写,而是你得想清楚:这个“周”到底以谁的时区为准、跨年怎么连、以及要不要为它多存一个字段来换性能。这些决定一旦上线,改起来比重写 SQL 还费劲。











