date_sub用错单位会导致结果错误:interval 1 month按日历月计算,interval 30 day为机械减天数;查上个月需手动构造起止日,且where条件须保持字段裸写以走索引。

DATE_SUB 用错单位会导致结果完全不对
MySQL 的 DATE_SUB 函数必须明确指定时间单位,写成 INTERVAL 1 MONTH 和 INTERVAL 30 DAY 行为完全不同——前者按日历月计算(比如 3 月 31 日减 1 月得 2 月 28 日),后者是机械减天数。很多业务要的是“上个月整月”,不是“30天前”,用 DAY 单位会跨月出错。
实操建议:
- 一律用
MONTH单位处理“上个月”逻辑,避免用DAY或WEEK - 如果字段是
DATETIME类型,注意DATE_SUB(NOW(), INTERVAL 1 MONTH)返回的是带时分秒的时间点,直接用于BETWEEN可能漏掉当天数据 - 测试时拿已知边界日期验证:比如当前是
2024-03-01,DATE_SUB('2024-03-01', INTERVAL 1 MONTH)应返回2024-02-01,而不是2024-02-01 00:00:00这种带时间的值(取决于你传入的是 date 还是 datetime)
获取上个月完整日期范围要自己算起止日
DATE_SUB 本身只返回一个时间点,没法直接给出“上个月第一天到最后一秒”。想查上个月全部数据,必须组合 DATE_SUB + LAST_DAY + DATE 函数手动构造区间。
常见错误现象:只写 WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 1 MONTH),这会把上个月 1 号 00:00:00 之前的数据全漏掉,实际查的是“近30天”,不是“上个月”。
实操建议:
- 上个月第一天:
DATE_SUB(DATE_SUB(CURDATE(), INTERVAL DAYOFMONTH(CURDATE())-1 DAY), INTERVAL 1 MONTH) - 上个月最后一天:
LAST_DAY(DATE_SUB(CURDATE(), INTERVAL 1 MONTH)) - 稳妥写法(含时间):
WHERE create_time >= DATE_SUB(DATE_SUB(CURDATE(), INTERVAL DAYOFMONTH(CURDATE())-1 DAY), INTERVAL 1 MONTH) AND create_time
不同数据库对 DATE_SUB 兼容性差异大
MySQL 支持 DATE_SUB(NOW(), INTERVAL 1 MONTH),但 PostgreSQL 要写 NOW() - INTERVAL '1 month',SQLite 根本没有 DATE_SUB,得用 date('now', '-1 month')。如果代码要跨库,别硬套 MySQL 写法。
使用场景:项目里混用 MySQL 和 TiDB(兼容 MySQL)没问题;但切到 PostgreSQL 或 Oracle 就会报错 FUNCTION DATE_SUB does not exist。
实操建议:
- 确认当前数据库类型再写,别凭经验复制粘贴
- 应用层拼接条件时,优先用参数化日期范围(如传入
start_date和end_date),把计算逻辑提到代码里,SQL 保持简单 - TiDB、阿里云 PolarDB(MySQL版) 可以照搬 MySQL 写法;但 AWS Aurora PostgreSQL 不行
WHERE 条件里用函数可能让索引失效
写成 WHERE DATE_SUB(create_time, INTERVAL 1 DAY) > '2024-01-01' 这类表达式,MySQL 无法走 create_time 字段的索引——因为对字段做了函数运算。性能会断崖式下跌。
正确做法永远是把函数挪到右边常量侧,让左边保持裸字段名。
实操建议:
- ❌ 错误:
WHERE DATE_SUB(create_time, INTERVAL 1 MONTH) >= '2024-01-01' - ✅ 正确:
WHERE create_time >= DATE_SUB('2024-01-01', INTERVAL -1 MONTH)(等价于>= '2024-02-01') - 用
EXPLAIN看执行计划,确认key列有索引名,rows数量级合理
上个月的边界计算看着简单,但月份天数不固定、时区、字段类型(DATE vs DATETIME)、索引是否生效,这几个点只要漏一个,查出来的数据就不是你要的。










