用DATEDIFF函数计算两个日期之间的天数、月数和年数

AI绘画32分钟前发布 jdkjadmin
0 00
用DATEDIFF函数计算两个日期之间的天数、月数和年数

用DATEDIFF函数计算两个日期之间的天数、月数和年数

计算两个日期相差多少天、多少个月或多少年,是数据库查询中的常见需求:统计订单间隔、筛选最近一段时间的数据、计算服务年限,或者判断某个日期是否达到年龄门槛。在 SQL Server 中,DATEDIFF 可以按指定的日期部分计算两个日期之间跨过了多少个边界。

不过,DATEDIFF 有一个容易被误解的特点:它计算的是日期部分边界的数量,不是把两个时间点相减后再换算成完整天数、完整月数或完整年数。理解这一点,才能正确选择写法,也能避开年龄、月末日期和时间精度等常见陷阱。

本文适用范围:SQL Server

本文讨论的是 Microsoft SQL Server 的 DATEDIFF 函数。其他数据库也可能有同名函数,但参数顺序、单位写法和计算规则未必相同。例如,不能直接把这里的 SQL Server 语法照搬到 MySQL、PostgreSQL 或 SQLite 中。

SQL Server 的基本语法如下:

DATEDIFF ( datepart , startdate , enddate )
  • datepart:要统计的日期部分,如年、月、日。
  • startdate:起始日期或时间。
  • enddate:结束日期或时间。

结果是从 startdate 到 enddate 跨过的指定边界数。把两个日期的位置交换,结果通常会改变符号:从较早时间算到较晚时间通常为正数,反过来则为负数。

先弄清楚“边界数”是什么意思

看一个常见的例子:两个时间点只相差一百纳秒,但它们分别位于两个不同的年份、月份和日期。按年、月、日计算时,DATEDIFF 的结果都可能是 1。原因是这个极短的时间间隔跨过了相应的日历边界。

SELECT
    DATEDIFF(year,  '2024-12-31T23:59:59.9999999', '2025-01-01T00:00:00.0000000') AS year_boundaries,
    DATEDIFF(month, '2024-12-31T23:59:59.9999999', '2025-01-01T00:00:00.0000000') AS month_boundaries,
    DATEDIFF(day,   '2024-12-31T23:59:59.9999999', '2025-01-01T00:00:00.0000000') AS day_boundaries;

在支持上述精度的 SQL Server 环境中,这三个结果都是 1。它们并不表示经过了一整年、一个整月或完整的二十四小时,而是表示期间分别跨过了一个年界、月界和日界。

因此,业务中的“相差多少天”可能有不同含义:

  • 跨过了多少个午夜:可使用 DATEDIFF(day, ...)。
  • 经过了多少个完整的 24 小时:应根据两个时间点的实际间隔计算,不能直接把 DATEDIFF(day, ...) 当作完整时长。
  • 经过了多少个完整月或完整年:需要在边界数量的基础上判断是否已经到达对应日期,不能只看 DATEDIFF(month/year, ...)。

先确定业务口径,再选函数,往往比先写 SQL 再解释结果更可靠。

计算两个日期之间跨过的天数

如果需求是计算两个值之间跨过了多少个日历日期边界,可以使用 DATEDIFF(day, startdate, enddate)。下面的脚本可直接在 SQL Server 中运行:

DECLARE @startdate date = '2024-03-01';
DECLARE @enddate   date = '2024-03-10';

SELECT DATEDIFF(day, @startdate, @enddate) AS day_boundaries;

结果为 9,因为从 3 月 1 日到 3 月 10 日跨过了 9 个午夜。若把两端日期都作为要统计的自然日,通常会把结果加 1:

SELECT
    DATEDIFF(day, @startdate, @enddate) + 1 AS inclusive_calendar_days;

这里的“加 1”只适用于两端都需要计入的日历日统计。它不表示时间段实际多经过了一天。还要留意起止顺序:如果结束日期早于开始日期,直接加 1 可能不符合业务预期,应先定义负区间如何处理。

有时间部分时的天数结果

对 datetime、datetime2 等包含时间的值,DATEDIFF(day, ...) 仍然计算跨过的午夜数量。例如,晚上 23:59 到第二天凌晨 00:01 只经过两分钟,但结果为 1;同一天凌晨 00:01 到晚上 23:59 经过了近 24 小时,但结果为 0。

SELECT
    DATEDIFF(day, '2024-03-01T23:59:00', '2024-03-02T00:01:00') AS crossed_midnights,
    DATEDIFF(day, '2024-03-01T00:01:00', '2024-03-01T23:59:00') AS crossed_midnights_same_date;

如果要筛选“最近 24 小时”的记录,应该按时间点比较,而不是用日边界代替时长。常见的写法是计算一个明确的起始时间,再与时间列比较:

DECLARE @now datetime2(0) = SYSDATETIME();

SELECT @now AS current_time,
       DATEADD(hour, -24, @now) AS start_of_last_24_hours;

实际查询时,可以将时间列与这个起始值比较。这样的区间表达式也通常比对时间列逐行套用 DATEDIFF 更有利于索引使用,具体仍应结合查询计划和数据类型验证。

计算两个日期之间跨过的月数

按月统计边界时,使用 DATEDIFF(month, startdate, enddate):

DECLARE @startdate date = '2024-01-15';
DECLARE @enddate   date = '2024-04-02';

SELECT DATEDIFF(month, @startdate, @enddate) AS month_boundaries;

结果为 3。这表示从 1 月到 4 月跨过了 2 月、3 月和 4 月的月初边界,并不代表已经完整经过三个月。即使结束日期早于开始日期中的日号,只要月份边界不同,结果仍按边界计算。

例如,从 1 月 31 日到 2 月 1 日只过了一天,但 DATEDIFF(month, ...) 返回 1。相反,从 1 月 1 日到 1 月 31 日虽然相隔三十天,结果却是 0,因为两个日期位于同一个月份。

只计算完整经过的月数

如果业务要求的是“完整经过了几个月”,仅用 DATEDIFF(month, ...) 会高估尚未到达月内对应日期的区间。可以先计算月边界数,再判断结束日期是否已经到达开始日期对应的日号和时间。下面示例适用于起始日期不晚于结束日期的情形:

DECLARE @startdate date = '2024-01-31';
DECLARE @enddate   date = '2024-02-28';

DECLARE @month_boundaries int = DATEDIFF(month, @startdate, @enddate);

SELECT
    @month_boundaries
    - CASE
        WHEN DATEADD(month, @month_boundaries, @startdate) > @enddate THEN 1
        ELSE 0
      END AS completed_months;

此例结果为 0:从 1 月 31 日到 2 月 28 日已经跨过一个月初边界,但还没有到达按 SQL Server 的 DATEADD 规则计算出的下一个月对应日期。对于 1 月 31 日加一个月,目标月份没有 31 日时,DATEADD 会返回目标月的最后一天;因此这里与 2 月 28 日比较,结果为 0 这一口径并不成立:实际 DATEADD(month, 1, '2024-01-31') 是 2024-02-29,故到 2 月 28 日为 0,到 2 月 29 日为 1。

“完整月数”的业务口径并不总是统一。有人按月末对月末视为一个完整月,有人要求对应日到达,还有人按固定天数折算。应先明确规则,再验证月末、闰年和负区间等情况。若要处理任意方向的起止日期,也要明确是返回带符号的完整月数,还是先规范化起止顺序后计算。

计算两个日期之间跨过的年数

按年边界统计时,使用 DATEDIFF(year, startdate, enddate):

DECLARE @startdate date = '2020-12-31';
DECLARE @enddate   date = '2021-01-01';

SELECT DATEDIFF(year, @startdate, @enddate) AS year_boundaries;

结果是 1,因为两个日期分别位于两个年份。它们实际只相差一天,所以这个结果不能直接解释为“已经完整经过一年”。同样,跨过多个元旦的边界数也不必然等于完整年数。

计算完整年数时,可以先计算年边界,再检查结束日期是否已到达起始日期的月日。下面示例按“生日当天年满一岁”的常见规则计算两个日期之间的完整周年数,并假设起始日期不晚于结束日期:

DECLARE @startdate date = '2000-10-20';
DECLARE @enddate   date = '2024-10-19';

DECLARE @year_boundaries int = DATEDIFF(year, @startdate, @enddate);

SELECT
    @year_boundaries
    - CASE
        WHEN DATEADD(year, @year_boundaries, @startdate) > @enddate THEN 1
        ELSE 0
      END AS completed_years;

结果为 23,因为结束日期还没有到 2024 年的周年日。若将结束日期改为 2024 年 10 月 20 日,结果为 24。

年龄计算的闰日规则

用出生日期计算年龄时,除了比较月日,还要定义 2 月 29 日出生者在非闰年何时视为达到生日。不同地区、业务制度或产品规则可能选择 2 月 28 日或 3 月 1 日,不能假定有一个适用于所有场景的唯一答案。

使用 DATEADD(year, ...) 计算周年日期时,闰日落在非闰年的情况会涉及 SQL Server 对目标日期的处理规则。应针对业务选定规则编写明确逻辑,并用 2 月 28 日、2 月 29 日、3 月 1 日等测试数据验证。若结果用于法律、保险、资格或计费判断,还应以相应制度定义为准,而不是仅凭一个通用 SQL 表达式。

参数限制与可用日期部分

datepart 要写成 SQL Server 支持的日期部分关键字或其缩写。常用单位及写法如下:

含义常用写法常见缩写
年yearyy、yyyy
季度quarterqq、q
月monthmm、m
年内日序dayofyeardy、y
日daydd、d
周weekwk、ww
小时hourhh
分钟minutemi、n
秒secondss、s
毫秒millisecondms
微秒microsecondmcs
纳秒nanosecondns

上表列出的是 SQL Server 支持的常见形式,具体写法应与目标 SQL Server 版本和实际需求相符。特别要注意,weekday 虽然也是受支持的日期部分,但它统计的是周边界,并不等同于“工作日数”;节假日、周末和企业日历都需要另外建模。

DATEDIFF 的 datepart 不能写成字符串,也不能作为普通变量或表达式传入。例如,以下写法不成立:

-- 错误示例:datepart 不能用字符串代替
SELECT DATEDIFF('day', '2024-01-01', '2024-01-02');

-- 错误示例:datepart 不能作为变量传入
DECLARE @part nvarchar(10) = N'day';
-- SELECT DATEDIFF(@part, '2024-01-01', '2024-01-02');

正确做法是直接写日期部分:

SELECT DATEDIFF(day, '2024-01-01', '2024-01-02') AS day_boundaries;

返回类型、范围和 DATEDIFF_BIG

DATEDIFF 返回 int。当计算结果超出 int 的范围时会发生溢出错误。可表示的最大间隔还取决于所选日期部分:粒度越细,较短的时间跨度就可能跨过大量边界。因此,对跨度很长的时间数据使用秒、毫秒等单位时,尤其要评估是否会超出范围。

SQL Server 2016 及更高版本提供 DATEDIFF_BIG,它返回 bigint,适用于结果可能超出 int 范围的计算:

SELECT DATEDIFF_BIG(second, '2000-01-01T00:00:00', '2024-01-01T00:00:00')
       AS seconds_between;

DATEDIFF_BIG 扩大的是结果类型范围,并不会把边界计数变成精确的物理时长换算,也不会改变其按指定日期部分统计边界的基本语义。纳秒等细粒度下,仍需考虑输入类型的实际精度和 SQL Server 对日期时间值的表示方式。

日期参数的可接受范围还受参数数据类型影响。例如,date、datetime、smalldatetime 和 datetime2 的范围、精度并不相同。如果字符串需要隐式转换,格式和会话语言设置也可能影响解析。建议将应用程序传入的日期作为有明确类型的参数,而不是依赖不明确的字符串格式。

常见错误与处理方式

错误一:把边界数当成完整时长

DATEDIFF(day, ...) 不是完整经过的 24 小时数,DATEDIFF(month, ...) 不是完整月数,DATEDIFF(year, ...) 也不是完整周年数。遇到“完整经过”“年满”“至少 30 天”等措辞时,应把边界结果与实际日期或时间比较,或直接构造业务定义的截止时间。

错误二:误以为结束日期会自动计入

DATEDIFF(day, '2024-03-01', '2024-03-10') 返回 9,而不是 10。前者是边界数;如果需要统计首尾都包含的自然日,才可能使用结果加 1。若数据包含时分秒,还要进一步明确端点是否包含,不能机械地给所有结果加一。

错误三:使用不明确的日期字符串

推荐使用明确的参数类型,或采用不容易产生歧义的 ISO 格式,例如 YYYY-MM-DD,以及带有 T 分隔符的日期时间文本。不要依赖诸如 03/04/2024 这样的格式来判断它表示 3 月 4 日还是 4 月 3 日。

错误四:混用不同的日期时间类型

起止参数类型不同时,SQL Server 会依据类型优先级等规则进行转换。转换可能影响精度,某些类型还支持范围不同。进行关键计算时,尽量让两侧使用相同且合适的类型;从应用端传参时也应指定参数类型,而不是把所有值都作为字符串发送。

错误五:认为 DATEDIFF(day) 等于工作日数

DATEDIFF(day, ...) 计算日历日边界,不会自动排除周六、周日或法定假日。工作日统计需要明确工作日历、节假日数据和首尾计入规则,常见做法是维护日历表并按表查询。

错误六:用函数包住查询列造成筛选性能问题

例如,下面的条件对表中每一行的列计算 DATEDIFF:

-- 可能不利于利用 event_time 上的普通索引
SELECT *
FROM dbo.Events
WHERE DATEDIFF(day, event_time, @now) <= 7;

这类写法还可能把未来时间的负数差值也纳入结果,不一定符合“最近七天”的含义。若要查一个明确的时间范围,通常更容易表达成列与边界值比较:

DECLARE @now datetime2(0) = SYSDATETIME();

SELECT *
FROM dbo.Events
WHERE event_time >= DATEADD(day, -7, @now)
  AND event_time <= @now;

这里表示从当前时刻往前七天到当前时刻之间,两个端点均包含。若业务需要左闭右开区间,可改为 event_time >= 起始时间 AND event_time < 结束时间,并据此处理相邻时间段,避免重复计入边界记录。索引是否被使用仍取决于索引、统计信息和完整查询计划,不能仅凭改写形式保证性能。

时区与夏令时:日历日不等于固定小时数

如果数据涉及多个时区,首先要统一时间语义。例如,数据库中保存的是 UTC 时间,还是带有时区偏移的本地时间?如果要计算用户所在地的自然日边界,应先明确时区,再按照该时区的日历规则处理。

DATEDIFF 面对包含时区偏移的值时,会考虑日期时间参数的偏移部分;但这并不等于它能替业务自动选定正确的时区,也不能替代对夏令时规则的设计。某些地区切换夏令时的日期,一天可能不是固定的 24 小时。若需求是“当地自然日”,应按目标时区构造自然日的起止时刻;若需求是经过了多少实际秒数,则应在统一的时间基准上计算。不要把这两种口径混为一谈。

动态日期单位与安全使用

日期值通常可以通过 SQL 参数传入;但 datepart 不是普通值参数,不能像日期那样直接用占位符替代。如果应用程序允许用户选择“按天、按月或按年”统计,不要把用户输入未经验证地拼接进 SQL 字符串。

更简单安全的方式是把可选单位映射到固定 SQL 分支,并继续对日期值使用参数化查询。例如,应用层将输入限制为允许的枚举值,再选择对应的固定语句。若确实需要动态构造 SQL,也必须对日期部分做严格白名单校验;不能因为它看起来像一个短字符串,就认为拼接没有注入风险。

下面是 T-SQL 中按白名单分支处理的示意代码。所有语句中的日期值仍作为参数使用;实际应用程序还应根据所用数据库驱动正确绑定参数:

DECLARE @unit nvarchar(10) = N'day';
DECLARE @startdate date = '2024-01-01';
DECLARE @enddate   date = '2024-01-10';

IF @unit = N'day'
    SELECT DATEDIFF(day, @startdate, @enddate) AS difference;
ELSE IF @unit = N'month'
    SELECT DATEDIFF(month, @startdate, @enddate) AS difference;
ELSE IF @unit = N'year'
    SELECT DATEDIFF(year, @startdate, @enddate) AS difference;
ELSE
    THROW 50001, 'Unsupported date unit.', 1;

该示例展示的是单位白名单,不是用户权限控制。DATEDIFF 本身只是日期计算函数,不会加密数据、验证身份、限制访问或防止 SQL 注入。安全性来自参数化查询、白名单校验、适当的数据库权限和应用程序的输入处理;把日期部分写死或按白名单选择,只是避免把不受信任的输入当成 SQL 语法的一种做法。

按需求选择计算方式

业务需求建议思路需要确认的边界
跨过多少个午夜DATEDIFF(day, startdate, enddate)是否把首尾自然日都计入
跨过多少个元旦DATEDIFF(year, startdate, enddate)结果不是完整年数
跨过多少个月初DATEDIFF(month, startdate, enddate)结果不是完整月数
经过多少个完整月或完整年边界数加对应日期比较月末、闰日、负区间规则
最近 24 小时内的数据计算时间阈值后直接比较时间列端点是否包含、时区语义
工作日或营业日数量使用明确的日历表或业务规则周末、节假日和地区日历

总结

在 SQL Server 中,DATEDIFF 的核心用法是 DATEDIFF(datepart, startdate, enddate)。用 day、month、year 分别计算两个值之间跨过的日、月、年边界,结果可以为正数、负数或零;如果结果可能超出 int 范围,并且 SQL Server 版本支持,可以考虑 DATEDIFF_BIG。

最重要的区分是:边界数不等于完整时长。计算自然日边界、完整月数、年龄、最近 24 小时和工作日,各自对应不同的业务口径。明确起止顺序、端点是否包含、月末与闰日规则、日期类型和时区后,再选择 SQL 表达式。对于动态日期单位,应使用固定分支或白名单,而不是直接拼接未经验证的用户输入。这样才能让结果既符合业务含义,也更容易测试和维护。

© 版权声明

相关文章