根本原因是date_format纯字符串截取,不感知日历逻辑;按周分组应使用yearweek(order_time,1)等语义化函数,避免'%y-%u'导致跨年错乱。

MySQL 用 DATE_FORMAT 按天/周/月分组时,为什么结果总少一天或跨年错乱?
根本原因是 DATE_FORMAT 纯字符串截取,不感知日历逻辑。比如 DATE_FORMAT(order_time, '%Y-%m') 看似按月,但遇到跨年(如 2024-12 和 2025-01)会正常分组;可一旦用 '%Y-%u'(周编号)就出问题:MySQL 默认周从周日开始,且第 1 周是包含 1 月 1 日的那一周——这意味着 2024-01-01(周一)所在周会被算作 2024 年第 1 周,但 2023-12-31(周日)却属于 2024 年第 53 周,导致同一自然周被拆到两个年份里。
实操建议:
- 按天统计:直接用
DATE(order_time)最稳,它返回DATE类型,无时区歧义,索引也能用上 - 按自然周(周一至周日):用
YEARWEEK(order_time, 1),第二个参数1表示“周一为每周起点,第 1 周含 4 天及以上” - 按自然月:别用
'%Y-%m'拼字符串,改用YEAR(order_time) * 100 + MONTH(order_time)得到整数 202401、202402……避免字符串排序陷阱 - 如果必须用
DATE_FORMAT,确保格式符带年份,例如周用'%x-%v'(ISO 周年+ISO 周序号),但要注意 MySQL 5.7+ 才完整支持
Oracle 或 PostgreSQL 里该用 TRUNC 还是 EXTRACT?
TRUNC 是日期截断,EXTRACT 是字段抽取,二者语义不同。比如想按月聚合,TRUNC(order_time, 'MM') 返回当月第一天(如 2024-03-01),可用于分组和排序;而 EXTRACT(YEAR FROM order_time) * 100 + EXTRACT(MONTH FROM order_time) 只返回数字,丢失日期语义,无法用于时间范围筛选。
实操建议:
- Oracle:优先用
TRUNC(order_time, 'DD')(当天零点)、TRUNC(order_time, 'IW')(ISO 周一零点)、TRUNC(order_time, 'MM')(当月一日)。注意'WW'是日历年周,'IW'才是 ISO 标准周 - PostgreSQL:没有
TRUNC的日期模式,改用DATE_TRUNC('day', order_time)、DATE_TRUNC('week', order_time)(默认周一为起点)、DATE_TRUNC('month', order_time) - 跨数据库兼容写法:统一转成字符串再 cast,但性能差,仅作兜底,例如
TO_CHAR(order_time, 'YYYY-MM')(Oracle) vsTO_CHAR(order_time, 'YYYY-MM')(PG),MySQL 则用DATE_FORMAT(order_time, '%Y-%m')
分组后查「当周业务量环比」,为什么 LAG() 算出来总是 NULL?
常见错误是分组键没对齐:LAG() 是窗口函数,依赖 ORDER BY 序列。如果按 YEARWEEK(order_time, 1) 分组,但 ORDER BY 写的是 DATE_FORMAT(order_time, '%Y-%u'),两者计算逻辑不一致,导致排序错位,前一行根本不是上周数据。
实操建议:
- 分组字段和窗口排序字段必须严格一致,例如都用
YEARWEEK(order_time, 1),别混用字符串格式化结果 - 确保数据覆盖连续时间段,如果某周无数据,
LAG()会跳过空缺直接取上上周——需先用GENERATE_SERIES(PG)或递归 CTE(MySQL 8.0+)补全周序列 - 环比计算建议封装成子查询:外层按周分组聚合,内层用
LAG(sum_volume) OVER (ORDER BY week_key),避免在 GROUP BY 里直接套窗口函数引发语法错误
千万级订单表加了 DATE_FORMAT 就慢十倍,怎么破?
因为 DATE_FORMAT(order_time, '%Y-%m') 是函数索引失效操作,MySQL 无法利用 order_time 上的普通 B+Tree 索引。执行计划里看到 type: ALL 就是全表扫描。
实操建议:
- 建函数索引(MySQL 8.0.13+):
CREATE INDEX idx_order_month ON orders ((DATE(order_time))),括号写法才是函数索引 - 更通用做法:增加冗余字段
stat_date DATE,写入时用DATE(order_time)自动填充,然后在该字段建索引并用于分组 - 分区表方案:按
RANGE COLUMNS(stat_date)分区,每月一个分区,查询时自动 Pruning,比函数索引更彻底 - 避免在 WHERE 条件里对时间字段做任何函数操作,例如
WHERE DATE_FORMAT(create_time, '%Y-%m') = '2024-03'必须改成WHERE create_time >= '2024-03-01' AND create_time
日期分组看着简单,真正卡住人的往往是周逻辑的隐式规则、函数索引的括号语法、以及窗口函数与分组键的咬合精度——这些地方一错,结果不对或性能崩掉,都很难一眼看出来。










