为什么这个 DateTime 会溢出?

Why is there an Overflow in this DateTime?

我正在尝试创建一个动态日历,我可以在其中忽略周末并跳到从现在开始的 45 天

Declare @StartDate Date = '12/01/2015'

;With NumberList AS
(
        Select *
        From 
        (
            Select Rank() Over(Order By S1.Id, S2.Id) Number
            From master.dbo.SysObjects S1, master.dbo.SysObjects S2
        ) XX
        Where Number < 1000
)
,FullCalendar as
(
    select Cast (dateadd(day, number,  DATEADD(yy, DATEDIFF(yy,0,getdate()), 0)) as Date) CalendarDate
    from Numberlist
)
,CalendarWithNumbers As 
(
    Select CalendarDate, Row_Number() Over (Order By CalendarDate) DayNumber
    From FullCalendar
    Where ((DATEPART(dw, CalendarDate) + @@DATEFIRST) % 7) NOT IN (0, 1)
)
,StartingDate as
(
    Select *
    From CalendarWithNumbers
    Where 1=1
        And CalendarDate = @StartDate
)
Select @StartDate StartDate, CalendarDate NDaysFromNow, DayNumber
From CalendarWithNumbers 
Where DayNumber = (Select DayNumber + 45 from StartingDate)

但是,我收到一个溢出错误

StartDate  NDaysFromNow DayNumber
---------- ------------ --------------------
Msg 517, Level 16, State 1, Line 3
Adding a value to a 'datetime' column caused an overflow.

但是,我只选择了我的第一个CTE中的前1000个数字,并且我的开始日期是一年的第一天。 1/1/2015 + 1000 天只到 9/26/2017.

为什么我会收到溢出错误?


编辑 - 添加注释以解释代码

我在代码中添加了注释以使其更易于理解。

Declare @StartDate Date = '12/01/2015'

;With NumberList AS
(
        /* Get a list of 1000 Numbers*/
        Select *
        From 
        (
            /* 
                Get a sequence of Numbers (I get 2071*2071 = 4289041) 
                I may need to go up to 50 years in the future so I have to do a cartesian product (same as Cross Join)
                However, this is where it fails
            */

            Select Rank() Over(Order By S1.Id, S2.Id) Number
            From master.dbo.SysObjects S1, master.dbo.SysObjects S2

            /*
                comment out the Select ABOVE 
                Uncomment the Select BELOW
                It works!!
                Probably because now the sequencing only goes up to 2071
            */
            --Select Rank() Over(Order By S1.Id) Number
            --From master.dbo.SysObjects S1

        ) XX
        Where Number < 1000
)
--Select * From NumberList
--UnComment just the line above, and comment everything below to see the result just up to there

,FullCalendar as
(
    /* Take the first day of the year, and add 1 to 1000 days to it
    It starts from 2015-01-02 (bug needs to be fixed so it starts from 2015-01-01)
    It ends at 2017-09-26
    */
    select Cast (dateadd(day, number,  DATEADD(yy, DATEDIFF(yy,0,getdate()), 0)) as Date) CalendarDate
    from Numberlist
)
--Select * From FullCalendar
--UnComment just the line above, and comment everything below to see the result just up to there

,CalendarWithNumbers As 
(
    /* 
    WHERE CLAUSE exclude the weekend days
    ROW_NUMBER now number each day sequentially and save it in the DayNumber column
    */
    Select CalendarDate, Row_Number() Over (Order By CalendarDate) DayNumber
    From FullCalendar
    Where ((DATEPART(dw, CalendarDate) + @@DATEFIRST) % 7) NOT IN (0, 1)
)
--Select * From CalendarWithNumbers
--UnComment just the line above, and comment everything below to see the result just up to there

,StartingDate as
(
    /* Get the DayNumber (from the query above) for the starting day*/
    Select *
    From CalendarWithNumbers
    Where 1=1
        And CalendarDate = @StartDate
)
--Select * From StartingDate
--UnComment just the line above, and comment everything below to see the result just up to there

/* 
    Now calculate 45 days from the daynumber in starting date
    that gives you the value for NDays from StartDate
*/
Select 
    SD.CalendarDate,
    Sd.DayNumber,
    CWN.CalendarDate NDaysFromNow, 
    CWN.DayNumber
From CalendarWithNumbers CWN
Inner Join StartingDate SD
    On CWN.DayNumber = SD.DayNumber + 345 

这行得通, 只是替换了生成 NumberList

的查询
Declare @StartDate Date = '12/01/2015'  --<-- I would use '20151201'

;With NumberList AS
(
        Select *
        From 
        (
            Select ROW_NUMBER() Over(Order By (SELECT NULL)) Number
            From master..spt_values S1
        ) XX
        Where Number < 1000
)
,FullCalendar as
(
    select Cast (dateadd(day, number,  DATEADD(yy, DATEDIFF(yy,0,getdate()), 0)) as Date) CalendarDate
    from Numberlist
)
,CalendarWithNumbers As 
(
    Select CalendarDate, Row_Number() Over (Order By CalendarDate) DayNumber
    From FullCalendar
    Where ((DATEPART(dw, CalendarDate) + @@DATEFIRST) % 7) NOT IN (0, 1)
)
,StartingDate as
(
    Select *
    From CalendarWithNumbers
    Where 1=1
        And CalendarDate = @StartDate
)
Select @StartDate StartDate, CalendarDate NDaysFromNow, DayNumber
From CalendarWithNumbers 
Where DayNumber = (Select DayNumber + 45 from StartingDate)

我的猜测是 SQL 服务器决定以与您想象的不同的顺序执行操作,实际上是这样的:

;With NumberList AS
(
        Select *
        From 
        (
            Select Rank() Over(Order By S1.Id, S2.Id) Number
            From master.dbo.SysObjects S1, master.dbo.SysObjects S2
        ) XX
        Where Number < 1000
)

在此之前未过滤到 999 行;

select Cast (dateadd(day, number,  DATEADD(yy, DATEDIFF(yy,0,getdate()), 0)) as Date) CalendarDate

即使那是你期望发生的事情。如果你把它改成这样,错误就会消失:

;With NumberList AS
(
  Select top 999 row_number() Over(Order By (select null)) Number
  From master.dbo.SysObjects S1 cross join master.dbo.SysObjects S2
)

使用 top 通常是更好的选择,因为这可以让优化器更好地了解实际有多少行来自那里,而不必决定何时使用 Number > 过滤掉行= 1000(或者数字的分布可能是什么)