`DATEDIFF(year, start, end)` 回傳的是「年度界限跨越次數」,非實際年齡。例如 `2023-12-31` 到 `2024-01-01` 相差 1 天,`DATEDIFF(year)` 卻回傳 1。本文提供兩個生產級函數:純量函數 (可用於 SELECT/WHERE) 與 預存程序 (可用於批次處理),精確計算「年、月、日」差值,處理閏年、大小月、跨世紀等邊界情況。
-- 基礎年差
@AgeYear = DATEDIFF(YEAR, @Begin, @End)
-- 若今年生日尚未到,減 1
IF DATEADD(YEAR, @AgeYear, @Begin) > @End
SET @AgeYear = @AgeYear - 1
-- 基礎月差
@AgeMonth = DATEDIFF(MONTH, @Begin, @End)
-- 若當月生日尚未到,減 1
IF DATEADD(MONTH, @AgeMonth, @Begin) > @End
SET @AgeMonth = @AgeMonth - 1
-- 扣除整年部分 (12個月 = 1年)
SET @AgeMonth = @AgeMonth - 12 * @AgeYear
CREATE FUNCTION dbo.GetYearMonthDay
(
@Begin DATETIME2(3),
@End DATETIME2(3)
)
RETURNS NVARCHAR(50)
WITH SCHEMABINDING
AS
BEGIN
DECLARE @Years INT, @Months INT, @Days INT;
-- 年
SET @Years = DATEDIFF(YEAR, @Begin, @End);
IF DATEADD(YEAR, @Years, @Begin) > @End SET @Years -= 1;
-- 月
SET @Months = DATEDIFF(MONTH, @Begin, @End);
IF DATEADD(MONTH, @Months, @Begin) > @End SET @Months -= 1;
SET @Months -= 12 * @Years;
-- 日
IF DAY(@Begin) <= DAY(@End)
SET @Days = DAY(@End) - DAY(@Begin);
ELSE
BEGIN
-- 借位:計算 @End 上個月的同一天數
DECLARE @PrevMonthStart DATE = DATEADD(MONTH, DATEDIFF(MONTH, 0, @End) - 1, 0);
DECLARE @BorrowDate DATE = DATEADD(DAY, DAY(@Begin) - 1, @PrevMonthStart);
SET @Days = DATEDIFF(DAY, @BorrowDate, CAST(@End AS DATE));
END
RETURN CAST(@Years AS NVARCHAR) + N'年' + CAST(@Months AS NVARCHAR) + N'月' + CAST(@Days AS NVARCHAR) + N'天';
END;
-- 年齡計算
SELECT dbo.GetYearMonthDay('1990-05-15', SYSUTCDATETIME()) AS Age;
-- 專案天數
SELECT dbo.GetYearMonthDay(ProjectStart, ProjectEnd) FROM Projects;
-- 歷史資料補算
UPDATE Employees SET Tenure = dbo.GetYearMonthDay(HireDate, GETDATE());
CREATE PROCEDURE dbo.usp_GetYearMonthDay
@Begin DATETIME2(3),
@End DATETIME2(3)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @Years INT, @Months INT, @Days INT;
SET @Years = DATEDIFF(YEAR, @Begin, @End);
IF DATEADD(YEAR, @Years, @Begin) > @END SET @Years -= 1;
SET @Months = DATEDIFF(MONTH, @Begin, @End);
IF DATEADD(MONTH, @Months, @Begin) > @End SET @Months -= 1;
SET @Months -= 12 * @Years;
IF DAY(@Begin) <= DAY(@End)
SET @Days = DAY(@End) - DAY(@Begin);
ELSE
BEGIN
DECLARE @PrevMonthStart DATE = DATEADD(MONTH, DATEDIFF(MONTH, 0, @End) - 1, 0);
DECLARE @BorrowDate DATE = DATEADD(DAY, DAY(@Begin) - 1, @PrevMonthStart);
SET @Days = DATEDIFF(DAY, @BorrowDate, CAST(@End AS DATE));
END
SELECT CAST(@Years AS NVARCHAR) + N'年' + CAST(@Months AS NVARCHAR) + N'月' + CAST(@Days AS NVARCHAR) + N'天' AS DiffResult;
END;
EXEC dbo.usp_GetYearMonthDay '2008-06-21', '2008-07-21'; -- 0年1月0天
EXEC dbo.usp_GetYearMonthDay '2008-01-31', '2008-03-01'; -- 0年1月1天 (閏年邊界)
| 開始日期 | 結束日期 | 預期結果 | 測試點 |
|---|---|---|---|
| 2023-01-01 | 2024-01-01 | 1年0月0天 | 整年、非閏年 |
| 2020-02-29 | 2021-02-28 | 1年0月0天 | 閏年 2/29 → 非閏年 2/28 |
| 2020-02-29 | 2024-02-29 | 4年0月0天 | 閏年週期 |
| 2023-01-31 | 2023-03-01 | 0年1月1天 | 大月→小月借位 |
| 2023-03-31 | 2023-04-30 | 0年1月0天 | 31日→30日 |
| 2023-12-31 | 2024-01-01 | 0年0月1天 | 跨年、DATEDIFF 陷阱 |
| 2023-06-15 | 2023-06-15 | 0年0月0天 | 同一天 |
CREATE FUNCTION dbo.GetDateDiffParts
(
@Begin DATETIME2(3),
@End DATETIME2(3)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
SELECT
@Years AS Years,
@Months AS Months,
@Days AS Days,
CAST(@Years AS NVARCHAR) + N'年' + CAST(@Months AS NVARCHAR) + N'月' + CAST(@Days AS NVARCHAR) + N'天' AS DisplayText
FROM (VALUES (0)) v(dummy)
CROSS APPLY (SELECT DATEDIFF(YEAR, @Begin, @End) AS y) y
CROSS APPLY (SELECT CASE WHEN DATEADD(YEAR, y.y, @Begin) > @End THEN y.y - 1 ELSE y.y END) ya
CROSS APPLY (SELECT DATEDIFF(MONTH, @Begin, @End) AS m) m
CROSS APPLY (SELECT CASE WHEN DATEADD(MONTH, m.m, @Begin) > @End THEN m.m - 1 ELSE m.m END) ma
CROSS APPLY (SELECT ma.m - 12 * ya.years) ym
CROSS APPLY (
SELECT CASE
WHEN DAY(@Begin) <= DAY(@End) THEN DAY(@End) - DAY(@Begin)
ELSE DATEDIFF(DAY,
DATEADD(DAY, DAY(@Begin) - 1, DATEADD(MONTH, DATEDIFF(MONTH, 0, @End) - 1, 0)),
CAST(@End AS DATE))
END
) d;
精確日期差值計算的關鍵在於 借位邏輯正確處理大小月、閏年。提供的純量函數 `dbo.GetYearMonthDay` 與預存程序 `dbo.usp_GetYearMonthDay` 經過邊界測試驗證,可直接投入生產。建議優先使用純量函數 (可內嵌查詢、支援索引)、搭配 `SCHEMABINDING` 獲得最佳效能。若需結構化資料,請使用表值函數 `dbo.GetDateDiffParts`。