作为一名常年跟MySQL打交道的老开发,我几乎每天都要跟日期时间打交道。统计、报表、定时任务、过期判断,凡是涉及时间窗口的计算,基本绕不开DATE_ADD和DATE_SUB这两个函数。很多人嘴上说会用,真到写SQL的时候连参数顺序都能搞反,或者被INTERVAL的语法折磨到怀疑人生。这篇文章我干脆一次性讲透,从语法、参数、场景到高频坑,全给你梳理清楚,看完你也能写出干净的日期时间计算SQL。
这两个函数说白了就是在指定日期时间上增减一段间隔,返回一个新的日期时间。DATE_ADD是加上间隔,DATE_SUB是减去间隔。它们的价值在于:你不需要自己写一堆繁琐的年月日加减逻辑,也不用担心月末、闰年这些边界情况,数据库引擎会帮你处理好。比如你要算“三个月前的今天”,直接DATE_SUB(NOW(), INTERVAL 3 MONTH)就完事。如果手动写DATE_ADD(NOW(), INTERVAL -3 MONTH),其实结果也是一样的,因为负间隔就是减法,两者可以互换使用。这就是函数设计的对称之处,理解了这一点,你就能在不同风格的SQL代码里自由切换。
适用人群很明确:刚学SQL的新手、做数据分析的同事、后端开发写报表接口的老手,以及那些需要在业务代码里拼时间条件的工程师。把这俩函数用熟,你写时间范围查询会省掉一大半力气,还不容易出错。
1. 函数全貌:语法、参数与返回类型
1.1 标准语法结构全解
先看官方标准语法:
DATE_ADD(date, INTERVAL expr unit) DATE_SUB(date, INTERVAL expr unit)两个函数的参数结构完全一致,第一个参数是基准日期或日期时间,第二个参数是间隔表达式。间隔表达式由两部分组成:一个数值expr和一个单位unit。数值可以是整数、负数、小数甚至表达式,单位决定了这个数值代表的是天、小时、月还是年。
举个具体例子:
SELECT DATE_ADD('2025-05-20', INTERVAL 10 DAY); -- 结果:2025-05-30 SELECT DATE_SUB('2025-05-20', INTERVAL 10 DAY); -- 结果:2025-05-1010 DAY就是“10天”这个间隔。DATE_ADD往未来走,DATE_SUB往过去走。从功能上讲,DATE_ADD('2025-05-20', INTERVAL -10 DAY)等价于DATE_SUB('2025-05-20', INTERVAL 10 DAY)。知道了这个等价关系,代码风格就能统一,团队协作时也少一些理解偏差。
1.2 关键参数类型说明
date参数可以接受多种数据类型:DATE、DATETIME、TIMESTAMP都可以,甚至合法的日期时间字符串也行。传入字符串时,MySQL会自动完成隐式转换。但我要提醒一句:能用时间类型就用时间类型,千万别指望字符串格式五花八门时还能正确解析。比如'2025/05/20'这种斜杠格式MySQL勉强认识,'2025.05.20'这种就属于自找麻烦。
返回类型遵循一个规则:如果date参数是DATE类型且间隔单位不涉及小时、分钟、秒,返回的就是DATE;一旦间隔包含了HOUR、MINUTE、SECOND等时间部分,返回类型自动变成DATETIME。DATETIME类型参与计算时,结果也保持DATETIME。实战里很多人踩过这个坑:以为返回的一定是DATE,结果输出带着00:00:00,跟前端格式匹配不上,就在那排查半天。
expr参数还可以写成浮点数,比如INTERVAL 1.5 HOUR表示一个半小时。MySQL 会把这个数换算成整数时间单位存储,1.5 HOUR实际就是90 MINUTE。这个特性在计算平均耗时、增量时间时挺有用,但要注意换算后的精度情况,太刁钻的小数可能导致不可预期结果,能用整数尽量用整数。
1.3 函数的对称性与等价写法
DATE_ADD和DATE_SUB是对称存在的。真实开发里,我见过有人永远只写DATE_SUB,因为他的业务场景全是往前推时间;也见过有人只写DATE_ADD,需要用负数间隔来兼做减法。这两种风格都行,但从可读性角度,我推荐:
- 明确语义:加时间用
DATE_ADD,减时间用DATE_SUB,别混用负号 - 维护方便:如果后头同事要改逻辑,一眼就能看出是加法还是减法
再补充一个冷知识:DATE_ADD和DATE_SUB其实是 MySQL 中ADDDATE、SUBDATE的“完整版”。后两者有简写形式,比如ADDDATE(date, INTERVAL 1 DAY)等价于DATE_ADD(date, INTERVAL 1 DAY),而且ADDDATE(date, 5)这种只写天数间隔的写法只属于ADDDATE,DATE_ADD不支持省略INTERVAL关键字。所以如果你看到同事代码里写ADDDATE(create_time, 30),别惊讶,这是合法的,不过就是省略了单位,默认按天算,稍微有点隐晦。
2. 间隔表达式 INTERVAL 的深入拆解
2.1 所有单位类型详解
INTERVAL支持的单位非常全,这是它强大之处。官方文档列出的单位我整理成一张表,方便你查阅:
| 单位 | 含义 | 示例 |
|---|---|---|
| MICROSECOND | 微秒 | INTERVAL 5000 MICROSECOND |
| SECOND | 秒 | INTERVAL 30 SECOND |
| MINUTE | 分钟 | INTERVAL 15 MINUTE |
| HOUR | 小时 | INTERVAL 8 HOUR |
| DAY | 天 | INTERVAL 30 DAY |
| WEEK | 周 | INTERVAL 2 WEEK |
| MONTH | 月 | INTERVAL 6 MONTH |
| QUARTER | 季度 | INTERVAL 1 QUARTER |
| YEAR | 年 | INTERVAL 2 YEAR |
| SECOND_MICROSECOND | 秒+微秒组合 | INTERVAL '10.5' SECOND_MICROSECOND |
| MINUTE_MICROSECOND | 分+秒+微秒组合 | INTERVAL '10:20.5' MINUTE_MICROSECOND |
| MINUTE_SECOND | 分+秒组合 | INTERVAL '10:20' MINUTE_SECOND |
| HOUR_MICROSECOND | 时+分+秒+微秒组合 | INTERVAL '10:20:30.5' HOUR_MICROSECOND |
| HOUR_SECOND | 时+分+秒组合 | INTERVAL '10:20:30' HOUR_SECOND |
| HOUR_MINUTE | 时+分组合 | INTERVAL '10:20' HOUR_MINUTE |
| DAY_MICROSECOND | 天+时+分+秒+微秒组合 | INTERVAL '1 10:20:30.5' DAY_MICROSECOND |
| DAY_SECOND | 天+时+分+秒组合 | INTERVAL '1 10:20:30' DAY_SECOND |
| DAY_MINUTE | 天+时+分组合 | INTERVAL '1 10:20' DAY_MINUTE |
| DAY_HOUR | 天+时组合 | INTERVAL '1 10' DAY_HOUR |
| YEAR_MONTH | 年+月组合 | INTERVAL '2-3' YEAR_MONTH |
看到DAY_HOUR这种组合,可能有人已经有点晕了。它的格式规则其实很简单:前面是较大单位,后面是较小单位,中间用什么分隔符取决于具体单位。DAY_HOUR用空格,YEAR_MONTH用横杠,HOUR_MINUTE用冒号。组合单位在排期计算、任务调度窗口这种需要精确到秒的场合会用到,平时不常见,但面试的时候偶尔会抽出来问,知道有这么个东西心里不慌。
2.2 组合单位写法与注意事项
组合单位最容易出错的是格式,我挑两个高频的写一下:
-- 增加 1 天 10 小时 20 分钟 30 秒 SELECT DATE_ADD('2025-05-20 12:00:00', INTERVAL '1 10:20:30' DAY_SECOND); -- 结果:2025-05-21 22:20:30 -- 增加 2 年 3 个月 SELECT DATE_ADD('2025-05-20', INTERVAL '2-3' YEAR_MONTH); -- 结果:2027-08-20注意组合单位的引号不能丢。INTERVAL 1 10:20:30 DAY_SECOND这种写法绝对是错的,数值和组合单位之间必须用字符串包起来。就这个引号问题,我见过不止一个同事在代码评审时被指出来。
再提醒一点:不能叠着写。有人想加“1年2个月”居然写成INTERVAL 1 YEAR + 2 MONTH,这直接语法报错。正确的做法用YEAR_MONTH组合,或者干脆分两步写两个函数嵌套,虽然丑点但不会出错:
SELECT DATE_ADD(DATE_ADD('2025-05-20', INTERVAL 1 YEAR), INTERVAL 2 MONTH); -- 结果:2026-07-20在我的实际开发经验里,组合单位用的频率远低于单一单位。99%的业务场景拆成两步加更清晰,组合单位主要图一个“一步到位”,但代价是可读性下降。你需要在代码可读性和简洁性之间做个取舍,没有绝对答案,团队统一风格就好。
2.3 INTERVAL 的关键行为细节
间隔表达式的数值支持正负数,这是设计上非常优雅的一点。负数会让加法变减法,正数让减法变加法。所以理论上你可以只记一个函数,DATE_ADD(date, INTERVAL -x unit)就能覆盖所有场景。
对于MONTH、YEAR这类大单位,MySQL 在运算时有一个非常容易忽略的行为:结果会尽量保持基准日期的“日”不变,但如果目标月份没有这个日子,会取目标月份的最后一天。比如:
SELECT DATE_ADD('2025-01-31', INTERVAL 1 MONTH); -- 结果:2025-02-28 SELECT DATE_ADD('2024-01-31', INTERVAL 1 MONTH); -- 结果:2024-02-29(闰年)1月31日加一个月,2月没有31日,MySQL自动给你落到2月的最后一天。这个行为让很多人又爱又恨:爱的是它不会报错,恨的是结果可能跟你的直觉不一样。如果你预期的是“跟1月31日对应的2月31日”,在票务、订阅扣费等业务里这就需要特殊处理。我的建议是:凡是涉及月末日期加减的,先算清楚业务期望值,再决定要不要用 DATE_ADD 直接算。比如客户订阅月底到期,你要算下个月的扣费日,最好先用LAST_DAY()拿到当月最后一天,再基于最后一天加减,否则很容易出现连续几个月的扣费日漂移。
3. 高频实战:DATE_ADD 与 DATE_SUB 的典型应用
3.1 近N天/周/月数据统计
这是最常见的场景。你要统计近7天的订单量,直接这么写:
SELECT COUNT(*) FROM orders WHERE order_time >= DATE_SUB(NOW(), INTERVAL 7 DAY);注意,order_time是DATETIME类型,DATE_SUB(NOW(), INTERVAL 7 DAY)返回的也是DATETIME,类型匹配不会出幺蛾子。但如果你用CURDATE()(只返回日期部分),那就要小心边界。NOW()带时间,CURDATE()不带时间,混用在>=和<条件时可能会把当天零点到当前时刻的数据漏掉或重复统计,这个等到第4节配合索引一起细讲。
月初到现在的数据,可以用DATE_SUB配合DATE_FORMAT拼一个月的起点,也可以用DATE_ADD直接算下个月的起点:
-- 从本月1号到现在的数据 SELECT COUNT(*) FROM orders WHERE order_time >= DATE_FORMAT(NOW(), '%Y-%m-01') AND order_time < DATE_ADD(DATE_FORMAT(NOW(), '%Y-%m-01'), INTERVAL 1 MONTH);第二个条件里的DATE_ADD(CURDATE(), INTERVAL 1 MONTH)看起来是在当前日期上加一个月,实际因为先做了DATE_FORMAT取月初,所以得到的是下个月1号零点。配合<号,就能精确圈定整个月的范围,不漏一秒。这个写法的好处是不用关心月份天数,2月、闰年都自动处理。
3.2 时间戳与日期的双栖转换
日期时间字符串可以直接参与运算,UNIX_TIMESTAMP()出来的整数用FROM_UNIXTIME()转回时间类型后同样可以参与计算。有些人在代码里用DATE_ADD(FROM_UNIXTIME(create_time), INTERVAL 1 DAY)来算过期时间,我建议还是在SQL查询层尽量保持类型一致,别老在函数之间来回横跳。如果确实存的是整型时间戳,直接在应用层做加减更清爽,把整型加减算完再转回去:
-- 伪代码,假设 create_ts 是整型时间戳 SELECT FROM_UNIXTIME(create_ts + 86400) AS expire_time FROM user_orders;为什么说不要过度依赖 SQL 函数?因为一旦你钻进了DATE_ADD(FROM_UNIXTIME(...))这种写法,索引基本就废了。只要字段被函数包住,MySQL 大概率不会走索引。这个在第5节详细说,这里先留个印象。
3.3 会员到期、优惠券过期的判定
电商系统的会员到期时间计算,典型做法是开卡时存一个到期时间,或者每次操作时用缓存时间加延长天数。假设用户开通了30天会员,开卡时间存在created_at,那到期时间可以用:
SELECT DATE_ADD(created_at, INTERVAL 30 DAY) AS expire_at FROM memberships WHERE user_id = 123;优惠券同理,一张券从valid_from到valid_until,判断是否可用:
SELECT coupon_id, title FROM coupons WHERE DATE_SUB(NOW(), INTERVAL 0 DAY) BETWEEN valid_from AND valid_until;这个例子有点故意绕弯子。实际你只需要把NOW()跟两个字段比较就完了,不需要套函数。但文科生也能看出来,DATE_SUB(NOW(), INTERVAL 0 DAY)就是NOW()本身,写出来只是为了演示语法上的灵活性,没实际意义。真实项目里千万别这么写,纯粹增加阅读负担。
3.4 定时任务与周期性提醒
后端写定时任务扫描超时未支付的订单:
UPDATE orders SET status = 'cancelled' WHERE status = 'pending' AND created_at < DATE_SUB(NOW(), INTERVAL 30 MINUTE);这里的语义非常清晰:超过30分钟未支付的订单自动取消。DATE_SUB(NOW(), INTERVAL 30 MINUTE)算出一个时间点,凡是created_at早于这个点的,都触发超时。
如果你希望任务在凌晨执行,比如每天1点统计前一天的数据:
SELECT COUNT(*) FROM orders WHERE order_time >= DATE_SUB(DATE_FORMAT(NOW(), '%Y-%m-%d'), INTERVAL 1 DAY) AND order_time < DATE_FORMAT(NOW(), '%Y-%m-%d');这种写法把CURDATE()的概念显式化:当天零点往前推一天,就是昨天零点;当天零点就是范围的右边界,左闭右开。整个统计范围恰好覆盖昨天全天。
4. 常见报错与认知误区实录
4.1 语法报错:INTERVAL 位置写错
报错现象:Syntax error near 'INTERVAL'或Incorrect parameters in the call to DATE_ADD。
这种错十有八九是把参数顺序搞反了。有人会写成:
-- 错误写法 SELECT DATE_ADD(INTERVAL 1 DAY, '2025-05-20');正确顺序一定是基准日期在前,INTERVAL 表达式在后。这跟直觉“加多少时间”有点拧,但函数设计就是这么定的,记一次就不容易忘了。
实用公式:永远先写DATE_ADD(,然后敲基准日期/字段,再敲, INTERVAL,再敲数值和单位,右括号收尾。顺序别动,基本不会错。
4.2 字符串日期格式导致的隐式转换问题
MySQL的隐式转换有时很智能,有时很坑。比如:
-- 这种写法能跑,但结果可能不对 SELECT DATE_ADD('2025-13-45', INTERVAL 1 DAY);如果字符串严重不合法,MySQL直接返回NULL并在日志里留下 warning,而不会帮你修正。最诡异的是有些非法日期 MySQL 也能解析,比如'2025-02-30'在某些版本里能转换成三月初的值,这种行为会直接污染统计结果。所以从应用层传参过来时,尽量保证传的是DATETIME/TIMESTAMP类型,把字符串到日期的转换放在显式的STR_TO_DATE或CAST上:
SELECT DATE_ADD(CAST('2025-05-20 12:00:00' AS DATETIME), INTERVAL 1 DAY);4.3 月末日期的“神奇漂移”
前面说过加一个月可能落到下月最后一天。这不算报错,但很容易造成业务逻辑偏差。比如每个月的30号要生成下月账单日,你直接用DATE_ADD('2025-01-30', INTERVAL 1 MONTH)得到2025-02-28,然后下一次基于2月28日再算,得出3月28日,账期就一路漂移了。要是本意是“每月月底出账”,那就应该基于LAST_DAY(CURRENT_DATE)来算,而不是基于上个月的账单日。处理这类逻辑时,要问一句:加一个月到底是“日历上对应的日期”,还是“下一个月的月底”。两种业务语义完全不同,用错就出大事。
4.4 时区问题:存 UTC 还是存本地时间
NOW()跟CURRENT_TIMESTAMP都返回当前会话时区的时间。如果 MySQL 服务器的time_zone参数设置不对,DATE_SUB(NOW(), INTERVAL 1 DAY)算出的时间点跟真实世界差了8小时,统计出来的数据自然不准。
排查思路:
- 执行
SELECT NOW(), CURRENT_TIMESTAMP, @@session.time_zone, @@global.time_zone; - 如果是
SYSTEM,看操作系统时区是否正常 - 如果业务需要跨时区,建议统一存储 UTC 时间,展示时再转本地时间,绝不在数据库里做时区加减后再存
这点看着跟函数本身没直接关系,但只要你用DATE_ADD/DATE_SUB跟NOW()配合,时区就是绕不开的坑。我排查过不少线上数据差8小时的事故,最后定位全是时区配置问题而不是函数用错。
5. 性能影响与替代方案权衡
5.1 函数包裹字段导致索引失效
这条是性能层面最重要的提醒。只要你把索引字段包进函数,MySQL 大概率放弃走索引。比如:
-- 索引失效风险写 SELECT * FROM orders WHERE DATE_ADD(order_time, INTERVAL 1 DAY) > NOW();这个条件把order_time包在了DATE_ADD里,优化器很难把它转换成范围扫描。更好的写法是把计算挪到常量侧:
SELECT * FROM orders WHERE order_time > DATE_SUB(NOW(), INTERVAL 1 DAY);这么写,order_time是裸字段,DATE_SUB(NOW(), INTERVAL 1 DAY)是一个确定的常量(执行时算一次),B+树索引就能正常走范围扫描。这个原则不只是针对 DATE_ADD / DATE_SUB,而是所有 SQL 函数都适用。判断标准很简单:哪个字段被函数包住,哪个字段就难受。
5.2 NOW()、CURDATE()、CURRENT_TIMESTAMP 的取舍
同类函数过多也会让人困惑。说下区别:
NOW():返回DATETIME,包含当前日期和时间,精度到秒(可带小数)CURDATE():只返回当前日期,DATE类型CURRENT_TIMESTAMP:跟NOW()基本等价,在DEFAULT子句里更常用
在日期时间加减中,如果你只关心天级别统计,用CURDATE()可以避免时间部分带来的边界问题:
-- 查昨天的数据(不含今天零点) SELECT * FROM logs WHERE log_date >= DATE_SUB(CURDATE(), INTERVAL 1 DAY) AND log_date < CURDATE();而NOW()适合精确到秒的场景。混用两者时,特别注意log_date是否包含时间部分。如果log_date是DATE类型,用DATE_SUB(CURDATE(), INTERVAL 1 DAY)就够了;如果是DATETIME,用NOW()并调整边界会更严谨。
5.3 与 TIMESTAMPDIFF / DATEDIFF 的功能边界
DATE_ADD/DATE_SUB解决的是“一个时间点加一段间隔得到另一个时间点”,而TIMESTAMPDIFF/DATEDIFF解决的是“两个时间点之间的间隔有多大”。一个是正向运算,一个是反向运算,正好互补:
-- 计算两个时间相差多少天 SELECT DATEDIFF('2025-05-20', '2025-05-10'); -- 结果:10 -- 计算两个时间相差多少个月 SELECT TIMESTAMPDIFF(MONTH, '2025-01-15', '2025-06-10'); -- 结果:4TIMESTAMPDIFF按整单位截断,不满一个月的部分直接舍弃,这是它的语义。如果你用DATE_ADD('2025-01-15', INTERVAL 5 MONTH)再反向验证,得到2025-06-15,然后用TIMESTAMPDIFF(MONTH, '2025-01-15', '2025-06-15')就会得到 5。所以两者组合使用可以完成互逆逻辑的验证。写复杂统计SQL时可以这两组函数来回验证逻辑,能发现不少隐蔽偏差。
5.4 大规模数据下的替代策略
如果表数据量到了千万级,频繁用DATE_SUB(NOW(), INTERVAL N DAY)做过滤也没什么问题,只要索引列是裸的。但如果你在同一个查询里对多个时间字段做一堆加减,比如DATE_ADD(update_time, INTERVAL 2 HOUR) > DATE_SUB(NOW(), INTERVAL 1 DAY),这种写法既复杂又容易让优化器懵掉。更稳妥的做法是:把边界值算好再传入 SQL 查询。比如在 Java 或 Python 代码里先算好startTime和endTime两个变量,SQL 里直接WHERE update_time BETWEEN ? AND ?。好处很多:SQL 逻辑简单、索引友好、参数化查询防注入、业务规则在代码层可测试。SQL 函数适合快速实现、临时分析、报表脚本;应用层计算适合核心链路、性能敏感的接口。两者不矛盾,关键看场景。
6. 实操干货:写日期时间加减SQL的九条心得
6.1 统一用裸字段接常量
所有查询条件里,把函数尽量用在常量侧,索引字段保持“裸奔”。这是查询性能的第一原则。
6.2 能用 CURDATE 别用 NOW 的场景别搞反
如果统计维度是天,边界就用CURDATE()配DATE_SUB;如果要精确到秒,才用NOW()。写之前问一句:这个条件到底需不需要时间部分?需要是几点几分几秒吗?不需要就果断CURDATE()。
6.3 嵌套函数不要超过两层
嵌套超过两层,人脑基本就转不过来了。遇到复杂日期计算,宁可拆成两个查询或者在应用层算好,也别写个五层嵌套的“祖传SQL”。后头维护的人会骂娘的。
6.4 月末日期先想清楚业务语义
涉及会员月卡、账单周期时,用LAST_DAY()显式处理月末,不要依赖DATE_ADD的“自动落到月末”行为。这个行为在不同业务下可能是feature也可能是bug,别让它偷偷决定你的业务规则。
6.5 检查 INTERVAL 的引号与格式
组合单位必须用引号包起来。INTERVAL '2-3' YEAR_MONTH是对的,INTERVAL 2-3 YEAR_MONTH是错的。这个错误提示不一定直观,报错时会让你看半天语法。
6.6 排查返回 NULL 的常见原因
DATE_ADD返回NULL大概率是参数非法。优先检查:
- 第一个参数是不是合法日期时间
- 数值是不是超出单位允许范围
- 有没有在函数里传
NULL值(只要传了 NULL,结果一定是 NULL)
6.7 用 TIMESTAMPDIFF 反向验证逻辑
写完一串加减后,用TIMESTAMPDIFF或DATEDIFF验证一下结果是否符合预期。比如要“推后一个月”,算完再用TIMESTAMPDIFF(MONTH, 原日期, 新日期)看是不是1。这种逆向验证习惯能帮你省下不少线上bug排查时间。
6.8 不要在循环里逐行用函数
ORM 里循环几十次,每次执行一条带DATE_SUB的查询,这种写法性能会很差。日期范围计算适合一次性生成范围,比如在SQL里生成连续日期序列:
SELECT DATE_ADD('2025-05-01', INTERVAL seq.seq DAY) AS day FROM ( SELECT ones.num + tens.num * 10 + hundreds.num * 100 AS seq FROM ... -- 拼一个数字序列 ) seq;这种“连续日期序列生成”在报表补齐缺失日期时相当好用,一次调用生成一整张日历表。前提是数字序列辅助表要提前建好,别临时写大段笛卡尔积,性能还是可控的。
6.9 使用前先确定 MySQL 版本
不同 MySQL 版本对日期函数的实现细节略有差异,特别是INTERVAL组合单位的微秒精度和时区行为。线上生产环境升级后,最好把核心日期计算SQL过一遍回归测试,防的是函数行为变化导致的隐性数据错误。尤其是从 5.7 升到 8.0 的时候,TIMESTAMP的存储行为和时区处理都有调整,牵连着DATE_ADD/DATE_SUB的表现。
这个函数组合我实际用下来最大的体会是:它们不复杂,但边界情况极其丰富。真正让你翻车的往往不是函数本身,而是你以为自己理解了“加一个月”是什么意思。把业务语义想清楚,再把日期计算交给数据库,这套配合能让你少掉很多头发。
最后分享一个写这类 SQL 时养成的习惯:凡是涉及日期时间范围的条件,我都会额外写一行注释,标明这个条件的业务含义、期望边界和实测结果。比如order_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)下面注释一行“统计从当前时刻往前推7天(含),不含未来数据”。等到三个月后你回来看这段SQL,或者同事临时接手你的报表任务,这一行注释比什么都管用。日期函数本身写起来很快,真正值钱的是你沉淀下来的这些边界判断和业务约定。