写 SQL 这些年,如果说有一个函数让我又爱又恨,那非DATEDIFF莫属。几乎每个业务系统里都能见到它的身影:算工龄、统计留存、做报表的时间窗口切割、判断是否超时……但绝大多数人用DATEDIFF都停留在“能算出结果就行”的层面,真正理解它返回值语义、边界条件和跨数据库差异的人少之又少。
这篇文章我打算把DATEDIFF从头到尾扒一遍。不讲虚的,直接围绕语法、参数、数据库方言差异、真实业务场景、性能优化和踩坑实录展开,让新手能直接上手,让老手也能查漏补缺。无论你是后端开发、数据分析师还是数据仓库工程师,这篇内容都能让你少走弯路。
1. 为什么需要 DATEDIFF,它到底在算什么
1.1 从业务需求说起
先从一个最常见的需求切入:统计用户从注册到首次下单花了多少天。你会不会下意识写一句DATEDIFF(day, register_time, first_order_time)?我相信大部分人都是这个习惯。但有没有想过,为什么不用first_order_time - register_time直接相减?
这里面有个关键点:日期和时间在数据库里的存储本质上是数字或结构化对象。在 SQL Server 里,datetime就是 8 字节的数值,整数部分代表从基准日期起的天数,小数部分代表一天内的时间比例。如果你直接相减,得到的是一个datetime类型的内核值,比如1900-01-01 00:15:00,表面上看是“15分钟”,但拿到这个结果还得再转换才能用在业务逻辑里,而且语义极不清晰。DATEDIFF的价值就在这一步被体现出来——它把“两个时间点之间的间隔”直接换算成你指定的时间单位(天、小时、分钟、秒、甚至月份和年份),返回的是一个纯整数,方便直接参与运算和比较。
1.2 返回值语义没那么简单
很多人以为DATEDIFF(month, date1, date2)返回的是“两个日期之间整月的数量”。这话只对了一半。DATEDIFF真正计算的是:从 date1 到 date2,跨越了多少个指定的 datepart 边界。
什么叫“边界”?对于MONTH来说,每个月的 1 号 00:00:00 就是一个边界。对于YEAR来说,每年的 1 月 1 日 00:00:00 是一个边界。DATEDIFF数的是“撞到边界的次数”,而不是“完整经过了多少个单位时间”。
我举个例子,你马上就能理解这个语义差异有多重要:
SELECT DATEDIFF(YEAR, '2023-12-31', '2024-01-01') AS year_diff;看到这个结果别惊讶,它返回的是1。两个日期只差了 1 天,但跨过了从 2023 到 2024 年的那个“年边界”,所以DATEDIFF(YEAR, ...)判定为“相差 1 年”。用日历上的自然语言理解,这显然“不准”,但对数据库来说,它只在数边界。
再举一个每月边界的例子:
-- 下面这个查询返回 1,因为跨越了 2 月 1 日的月边界 SELECT DATEDIFF(MONTH, '2023-01-31', '2023-02-01') AS month_diff; -- 这个也返回 1,虽然中间隔了整整一年,但同样只跨越了 12 个边界中的第一个 SELECT DATEDIFF(MONTH, '2022-01-01', '2023-01-01') AS month_diff_same; # 实际返回 12看到没有,2023-01-31 到 2023-02-01 和 2022-01-01 到 2023-01-01 都返回 1?不对,第二个返回 12。但重点在于,1月31日到2月1日确实是“不足一个自然月”,却返回 1。这就是边界计数语义。如果你在做月末对账、周期计算,必须对这个行为有清晰的认知,否则线上统计结果会出现让你怀疑人生的偏差。
1.3 直接相减和 DATEDIFF 的差别
生活化类比一下:直接相减像是在量一段绳子的“实际长度”,而DATEDIFF像是在数这段绳子跨越了多少根刻度桩。绳子的实际长度往往大于刻度桩之间的距离,数桩子得到的结果在某些应用场景里是对的(比如算“跨了几周”),但在另一些场景里是错的(比如算“实际工作小时数”)。这个类比如果你能记住,后面遇到各种边界条件就不会再迷惑了。
2. 三大主流数据库的 DATEDIFF 实现与差异对比
如果你只在一种数据库上写 SQL,可能感觉不到DATEDIFF有什么坑。但一旦你经历过从 SQL Server 迁到 MySQL,或者 PostgreSQL 和 MySQL 混合使用的数据平台,你就会发现:不同数据库的 DATEDIFF 完全是不同的物种。下面按数据库类型逐一拆解,并给出一张可以直接参考的对比表。
2.1 SQL Server / T-SQL 的 DATEDIFF 和 DATEADD 配合
SQL Server 里的DATEDIFF(datepart, startdate, enddate)是功能最完整的实现之一。参数顺序是“单位在前,两个日期在后”,这和 MySQL 完全相反,也是面试高频坑。支持的单位非常多:YEAR、QUARTER、MONTH、DAY、HOUR、MINUTE、SECOND、MILLISECOND、WEEK、ISO_WEEK等等。
它的语法里还有个容易被忽略的细节:startdate和enddate两个参数,结果是用enddate - startdate的边界数来算的。也就是说,DATEDIFF(DAY, '2024-01-01', '2024-01-02')返回 1,而把两个参数调换位置就返回 -1。这在业务里意味着,如果你把起止日期传反了,得到的是一个负数,很多不严谨的代码会直接把这个负数拿去做算术,导致数据污染。
T-SQL 里还有一对黄金搭档:DATEDIFF和DATEADD经常配合使用来做时间“抹平”。比如你想把任意时间点归到当天零点,可以这样:
SELECT DATEADD(DAY, DATEDIFF(DAY, 0, GETDATE()), 0) AS today_start;这段 SQL 的思路是:先算从基准日1900-01-01到当前时间跨越了多少天,然后再把这个天数加回基准日,得到的结果就是今天零点。这个写法非常经典,做时间序列分组、日报统计时效率极高。理解了DATEDIFF的边界计数语义,你就能一眼看懂这段 SQL 的运作原理。
2.2 MySQL 的 DATEDIFF 只算天数
MySQL 的DATEDIFF(expr1, expr2)跟 SQL Server 比,参数顺序直接相反,而且没有单位参数,只返回两个日期之间相差的天数(expr1 - expr2的天数)。所以你在 MySQL 里想算“两个时间点相差多少小时”,就不能用DATEDIFF,得用TIMESTAMPDIFF(HOUR, date1, date2),这里的参数顺序又是“单位在前,日期在后”。
比如:
-- 返回 1,注意参数顺序是 expr1 - expr2 SELECT DATEDIFF('2024-01-02', '2024-01-01'); -- 返回 -1 SELECT DATEDIFF('2024-01-01', '2024-01-02'); -- 算小时差,必须用 TIMESTAMPDIFF SELECT TIMESTAMPDIFF(HOUR, '2024-01-01 12:00:00', '2024-01-01 15:30:00'); -- 返回 3TIMESTAMPDIFF其实和 SQL Server 的DATEDIFF在语义上非常接近,也是数边界。但 MySQL 还有一个TIMEDIFF,返回的是时间差值本身,类型是TIME,最大范围是 838:59:59,超过这个范围会报错。这一堆DIFF家族函数,新手很容易搞混。我个人在项目里的建议是:先确定你用的数据库,再看对应的函数文档,不要凭 SQL Server 的经验直接写 MySQL 的日期差查询。跨库迁移时,这些函数差异简直是埋雷重灾区。
2.3 PostgreSQL 用减法运算符和 EXTRACT 来实现
PostgreSQL 没有单独的DATEDIFF函数。它处理日期差的方式非常“程序员风格”:直接相减。两个日期相减得到integer类型的天数;两个timestamp相减得到interval类型,也就是“时间间隔”。
-- 日期相减,返回整数 1 SELECT DATE '2024-01-02' - DATE '2024-01-01'; -- 时间戳相减,返回 interval SELECT TIMESTAMP '2024-01-02 12:00:00' - TIMESTAMP '2024-01-01 12:00:00'; -- 返回 1 day那 PostgreSQL 里怎么得到“相差几个月”?答案是EXTRACT或date_part配合AGE函数:
-- 返回 1 年 0 个月 1 天 SELECT AGE(TIMESTAMP '2024-01-02', TIMESTAMP '2023-01-01'); -- 提取月份差 SELECT EXTRACT(YEAR FROM AGE(TIMESTAMP '2024-01-02', TIMESTAMP '2023-01-01')) * 12 + EXTRACT(MONTH FROM AGE(TIMESTAMP '2024-01-02', TIMESTAMP '2023-01-01')) AS month_diff;PostgreSQL 的AGE和DATEDIFF的语义有本质区别:AGE返回的是“日历上真实的年月日间隔”,比如从 2023-01-01 到 2024-01-02,AGE会说 1 年零 1 天;而 SQL Server 的DATEDIFF(MONTH, ...)同样参数会说 12 个月。大部分时候你希望得到的是“真实间隔”,但某些统计口径要求“边界计数”。你要根据自己的业务语义选择合适的实现方式,这也是为什么我说 DATEDIFF 不是“理所当然”就能用的函数。
2.4 对比小结
| 数据库 | 函数/运算符 | 参数顺序 | 是否支持单位 | 返回类型 | 典型示例 |
|---|---|---|---|---|---|
| SQL Server | DATEDIFF(datepart, start, end) | end - start | 是 | int | DATEDIFF(DAY,'2024-01-01','2024-01-02') |
| MySQL | DATEDIFF(start, end) | start - end | 否,仅天数 | int | DATEDIFF('2024-01-02','2024-01-01') |
| MySQL | TIMESTAMPDIFF(datepart, start, end) | end - start | 是 | int | TIMESTAMPDIFF(MONTH,'2024-01-01','2024-02-01') |
| PostgreSQL | date1 - date2 | date1 - date2 | 隐式(天/interval) | int/interval | DATE '2024-01-02' - DATE '2024-01-01' |
| PostgreSQL | EXTRACT(EPOCH FROM interval) | — | 是 | 数值 | 秒/小时换算 |
| Oracle | date1 - date2 | date1 - date2 | 隐式(天) | 数值 | 得到小数天,再乘 24 换算小时 |
如果你在企业里维护过数据平台的迁移,应该深知上述差异有多折磨人。避免这种折磨最好的做法是:在整个数据团队内部建立一组统一的“日期差计算函数”规范,明确规定哪类需求用哪个函数,并写成注释模板贴在代码仓库里。不要指望所有同学都能记住这些差异,靠流程规范才是最稳的。
3. 核心实操:真实业务场景中的完整写法
这一章我会直接上真实业务案例的 SQL。每个案例都来自我在项目里的实际应用场景,附上参数计算思路和注意事项。你完全可以复制回去改改表名就能用。
3.1 场景一:员工工龄按月计算
很多 HR 系统需要算员工“工作年限(月)”。最直接的做法是:
SELECT employee_name, hire_date, DATEDIFF(MONTH, hire_date, GETDATE()) AS months_of_service FROM employee;但是这里有个坑我必须要提醒你:DATEDIFF(MONTH, hire_date, GETDATE())不会考虑“不足一个月”的情况。比如某员工 1 月 31 日入职,今天 2 月 1 日,按自然月理解才工作了 1 天,但上面 SQL 返回 1 个月。这在很多团队的统计口径里是不能接受的。
如果你需要“整月”才计入工龄,应该用更严谨的判断:
SELECT employee_name, hire_date, DATEDIFF(MONTH, hire_date, GETDATE()) - CASE WHEN DAY(GETDATE()) < DAY(hire_date) THEN 1 ELSE 0 END AS completed_months FROM employee;这个逻辑的思想是:先数边界得到“跨了多少个月”,如果今天在这个月里还没到入职日对应的那天,就说明上个月那个边界其实还没真正“完成一个月”,减去 1。这就是典型的把 DATEDIFF 的边界语义换算成自然月语义的处理方式。你在任何论坛搜“DATEDIFF 工龄不准”,基本都会看到类似方案。
另一个更推荐的做法是用 PostgreSQL 的AGE函数,或者 SQL Server 里的DATEDIFF配合DATEADD来做校验,这里不展开全部写法,但核心原则你只需要记住:如果业务定义“满一个月”才叫一个月,DATEDIFF 的结果必须二次处理。
3.2 场景二:用户留存和活跃统计
留存统计是数据分析师的高频需求。比如我们要按“注册后第 N 天”统计用户活跃情况,很多人会写:
SELECT DATEDIFF(DAY, register_date, active_date) AS day_offset, COUNT(DISTINCT user_id) AS active_users FROM user_active_log GROUP BY DATEDIFF(DAY, register_date, active_date);这个写法可以算出来,但如果user_active_log表很大,DATEDIFF(DAY, register_date, active_date)作为GROUP BY字段会带来两个问题:一是计算成本高,二是难以走索引优化。更优的做法是在WHERE条件里预先圈定时间窗口,再分组。
SELECT DATEDIFF(DAY, register_date, active_date) AS day_offset, COUNT(DISTINCT user_id) AS active_users FROM user_active_log WHERE active_date >= '2024-01-01' AND active_date < '2024-02-01' GROUP BY DATEDIFF(DAY, register_date, active_date);不要小看这个WHERE条件的价值。在没有它的时候,数据库扫描的是全表所有记录;加上它之后,如果active_date有索引,扫描范围大幅缩小,分组计算量也成倍下降。对于千万级以上的日志表,这个优化带来的查询耗时差异可以从“分钟级”降到“秒级”。关于索引优化我会在第 4 章专门讲,这才是DATEDIFF用得不好引发性能灾难的重灾区。
3.3 场景三:跨月、跨周的自然周聚合
现在假设你是电商平台的数据分析师,想看每个订单“属于哪一周”来做周报。由于订单时间分布在各个时刻,直接用DATEDIFF(WEEK, order_time, '2024-01-07')这种写法很容易让人算晕。我更推荐先把每个订单时间“抹平”到它所在的周一或周日,再做分组。
SQL Server 里可以这样把时间抹到“周起始日”:
SELECT DATEADD(DAY, 1 - DATEPART(WEEKDAY, order_time), CAST(order_time AS DATE)) AS week_start, SUM(order_amount) AS total_amount FROM orders WHERE order_time >= '2024-01-01' AND order_time < '2024-03-01' GROUP BY DATEADD(DAY, 1 - DATEPART(WEEKDAY, order_time), CAST(order_time AS DATE));这里的DATEPART(WEEKDAY, order_time)返回的是该时间是星期几(SQL Server 默认周日为 1,周一为 2)。用 1 减去这个值,得到从当前日期回到本周日的天数差,再DATEADD就能拿到周一。(注意不同数据库的WEEKDAY起始日不同,需要根据数据库文档调整偏移量。)
很多人一开始看到这个写法会觉得复杂,但这就是数据库日记操作的典型套路:先用DATEADD和DATEPART构造出分组键,再进行聚合。只要把一次性的“查询需求”沉淀成一段可复用的 SQL 模板,后续不同日期的报表只是换一下WHERE参数,效率提升非常明显。
3.4 场景四:精确到小数的时间差和进度计算
有些场景需要算“精确的年数”或“进度百分比”。比如做项目管理,要看一个任务从开始到今天完成了百分之多少的计划工期。有些同学会用 DATEDIFF 去除以计划天数:
SELECT task_name, DATEDIFF(DAY, start_date, GETDATE()) * 1.0 / DATEDIFF(DAY, start_date, end_date) AS progress_ratio FROM tasks;这里有几个细节要注意。第一,DATEDIFF(DAY, start_date, GETDATE())是按“边界数”算的,如果任务是今天开始的,会返回 0;如果任务计划开始时间是凌晨 00:00,现在是下午 14:00,返回 1,导致出现“任务刚开始半天就算完成了 1 天进度”的偏差。第二,整数除法问题——SQL Server 里两个INT相除结果还是INT,所以必须乘以 1.0 转成浮点数,否则任何不足整个任务周期的进度都会被截断成 0 或 100%。第三,如果任务还没开始,DATEDIFF会返回负数,导致进度比出现负值,这在业务上要加CASE WHEN保护。
我的建议是:在做进度类计算时,优先使用“分钟数”或“小时数”这种更细粒度的单位,先把两个时间点换算成统一的数值,再相除。这样可以最大限度地减少边界计数带来的误差:
SELECT task_name, DATEDIFF(MINUTE, start_date, GETDATE()) * 1.0 / DATEDIFF(MINUTE, start_date, end_date) AS progress_ratio FROM tasks WHERE start_date IS NOT NULL AND end_date IS NOT NULL AND DATEDIFF(MINUTE, start_date, end_date) > 0;把单位从“天”细化到“分钟”,虽然计算量略增,但结果对短周期任务要准确得多。
4. DATEDIFF 的边界条件与性能优化实践
这一章要说的是那些文档里不会直接告诉你、但确实会在生产环境让你翻车的细节。我按频率从高到低整理,每一个都是我或者我认识的开发者踩过的真实坑。
4.1 当端点为 NULL 时的行为
DATEDIFF只要有一个参数是NULL,返回结果就是NULL。看上去这是“理所当然”的,但问题在于很多业务代码没有对NULL做保护,直接拿这个结果去进行加减比较。
举个例子,你要统计每个用户的注册到首单天数,如果该用户还没下过单,first_order_time是NULL,那么DATEDIFF(DAY, register_time, first_order_time)就是NULL。如果你在WHERE里写DATEDIFF(DAY, register_time, first_order_time) > 30,这个条件只会筛选出已经下过单且首次下单时间差大于 30 天的用户,从未下过单的用户会被排除在外。如果你想要的其实是“还没首单的用户数”,这个 SQL 就把他们漏掉了。
避免这个问题的办法不是改变DATEDIFF,而是先做WHERE first_order_time IS NOT NULL的显式过滤,再逐步处理NULL业务分支。很多生产环境的数据质量异常,追根溯源都不是DATEDIFF的问题,而是对NULL的语义没有想清楚。
4.2 用 DATEDIFF 包裹列会致命地拖垮查询性能
这是DATEDIFF最经典的性能陷阱。先看一个反面教材:
SELECT COUNT(*) FROM orders WHERE DATEDIFF(DAY, order_time, GETDATE()) <= 30;这段 SQL 的逻辑是找最近 30 天内的订单,看起来很合理。但实际上,只要你在WHERE条件里对order_time列应用了DATEDIFF函数,这个列上的索引就失效了。原因很简单:索引通常基于原始的列值构建,一旦你对列做函数转换,数据库必须扫描所有行、计算每个行的函数结果才能判断是否满足条件。这就是所谓的“函数导致索引失效”。
正确的写法是把函数变换放在表达式一侧的常量上:
SELECT COUNT(*) FROM orders WHERE order_time >= DATEADD(DAY, -30, GETDATE());这两条 SQL 效果相同,但执行计划天壤之别。第二条写法把order_time >= 某常量留给数据库,它可以直接走索引,只扫描这 30 天内的数据,性能对数亿行的表来说可能是“秒回”与“全表扫”的区别。
这个优化原则不仅适用于DATEDIFF,也适用于DATEPART、DATEADD等所有日期函数。我见过太多同事把WHERE DATEPART(YEAR, create_time) = 2024挂在线上,每到跑批时间就卡死。换成WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01'之后,问题立刻缓解。记住:索引不友好不是 DATEDIFF 的锅,而是你把它放错了位置。
4.3 闰年和月份天数带来的偏差
按月份或年份计算时,DATEDIFF 的边界语义会直接受底层历法影响。比如 2024 年是闰年,DATEDIFF(DAY, '2024-01-01', '2025-01-01')返回 366,而DATEDIFF(DAY, '2023-01-01', '2024-01-01')返回 365。这个不用多说。
但容易忽略的是DATEDIFF(MONTH, ...)配合月末场景。比如:
SELECT DATEDIFF(MONTH, '2024-01-31', '2024-02-29');2024 年是闰年,2 月有 29 号,结果是 1。而如果你换到非闰年:
SELECT DATEDIFF(MONTH, '2023-01-31', '2023-02-28');结果也是 1。注意,这两个结果都是“跨越了 2 月 1 日这条月边界”得到的,和“31 号到 28 号是不是满了 30 天”没有任何关系。如果你做的是自然月的等价判断,比如判断两个日期是否属于同一个账单周期,就要用其他方式,比如比较DATEADD(MONTH, DATEDIFF(MONTH, 0, date1), 0)是否相等。这类话题可以说是行业里常年讨论的热点,本质都是对 DATEDIFF 语义理解不一致导致的。
举一个真实翻车案例:某报表系统要统计“每名员工在过去 3 个完整月份内的请假天数”,开发直接用DATEDIFF(MONTH, 请假开始日, GETDATE()) <= 3来圈定范围,结果每逢月初统计口径就漂移。最后改成用“月初”固定时间点做WHERE条件,问题才彻底解决。这个教训告诉我们:时间范围的设计原则上应该基于“绝对时间锚点”,而不是相对GETDATE()的函数计算。
4.4 时区与夏令时的隐性影响
如果你的数据涉及多个时区,或者数据库服务器设置了UTC + 8而业务用户在东八区之外,那么DATEDIFF(DAY, ...)的结果可能在跨时区的场景下出现偏差。举个例子:一个订单在 UTC 时间的 2024-01-01 23:00:00 创建,对应的北京时间已经是 2024-01-02 早上 7 点。如果你在数据库层面直接比较“订单创建日期”和“用户访问日期”,很容易因为时间精度丢失而把同一天的订单算成前一天或后一天。
规避办法很简单:统一约定所有时间字段的存储标准(比如一律存 UTC,展示时再转),并且在写 DATEDIFF 前明确时间粒度。如果业务只关心自然日,可以先用CAST(order_time AS DATE)把时间部分“削掉”,再做日期差。这个操作能很大程度上减少时区带来的隐性错误。另外,少数地区有夏令时调整,跨夏令时边界的DATEDIFF(HOUR, ...)结果可能比实际感知的小 1 小时,这在排障时需要特别留意。
5. 常见问题速查表与排查思路
最后这部分我整理成一张速查表,方便你遇到问题时直接对照排查。每一行都是一次真实的排障经历浓缩出来的产物。
| 问题现象 | 可能原因 | 排查思路与解决 |
|---|---|---|
| DATEDIFF 返回负数 | 参数传反了 | 检查 startdate 和 enddate 的位置,确认到底是用 end - start 还是 start - end |
| 算出的月数比预期多了 1 | 没有处理“末月不足”的情况 | 在 DATEDIFF 基础上做 CASE WHEN DAY(GETDATE()) < DAY(start_date) THEN 1 ELSE 0 END 修正 |
| 查询超时、全表扫描 | WHERE 中对索引列套了 DATEDIFF | 改为WHERE order_time >= DATEADD(DAY, -30, GETDATE())形式 |
| 结果为 0 但期望是 0.x 天 | 单位粒度过粗 | 把 DAY 改成 HOUR 或 MINUTE,或者用带小数的计算方式 |
| 两个日期跨月但自然月不足 30 天 | 边界计数语义导致 | 明确业务到底是要“实际间隔”还是“边界数”,二选一 |
| 时间差和业务记录对不上 | 时区 / UTC 混乱 | 统一存储标准,比较前显式转日期或用AT TIME ZONE换算 |
| 闰年 366 天导致同比环比异常 | 日历天差异正常 | 同比环比时优先用DATEADD(YEAR, -1, ...)锚定去年同一天 |
| DATEDIFF 与 TIMESTAMPDIFF 混用 | 数据库方言差异 | 迁移时做函数映射,代码审查时人工核对 |
| 结果为 NULL 导致统计缺失 | 端点为 NULL | 先WHERE col IS NOT NULL,再处理业务上的“无值”逻辑 |
| 计算长时间任务时精度不准 | DATEDIFF 数边界导致累计误差 | 改用秒或毫秒单位,再按需换算 |
排查这类问题,我自己的经验是三步走。第一步,先把 SQL 里的日期参数单独SELECT出来,人工和日历对照一遍,确认日期本身没有错。第二步,换一个更细粒度的时间单位重新算一遍,比如把MONTH换成DAY,如果结果和直觉差距过大,基本上就是边界计数的问题。第三步,针对WHERE条件的写法做执行计划分析,看是不是函数导致索引失效。这三步能解决 90% 的 DATEDIFF 相关故障。
写在后面:DATEDIFF 的正确打开方式
可能有人会觉得,一个DATEDIFF有什么好写几千字的?但它在生产环境里引发的“灵异事件”远比想象中多得多。我自己就经历过一次:某次月度报表数据对不上,查了两天,最后发现是一个同事在 MySQL 里用了DATEDIFF当 SQL Server 的DATEDIFF用,参数顺序完全写反,导致所有月份差都是负数,然后下游代码又用这个负数做了绝对值处理,最终统计结果足足多算了 2 倍。这个经历让我意识到,函数本身不难,难的是团队里每个人对函数语义的认知是否一致。
所以我对你的建议是:无论你用什么数据库,花 10 分钟把你当前使用的DATEDIFF相关函数完整读一遍文档,重点看它的“参数顺序”“返回单位语义”“对 NULL 的处理”,然后写几组极端日期用例自己测一下。这个举动投入产出比极高,能帮你规避掉未来至少三次以上的线上事故。如果你有权限维护公共代码库,也建议把常用日期计算的规范模板沉淀下来,让团队都走同一条路径。SQL 里的日期计算永远比表面看起来更微妙,而小心求证,就是我们这些老家伙能在生产环境里存活下来的原因。