【问题标题】:T-SQL Calculate recurrences of a DateT-SQL 计算日期的重复次数
【发布时间】:2017-04-13 09:58:29
【问题描述】:

我正在研究计算一周重复次数的 CTE,但是当 模式 跨年时我遇到了一些问题。

CTE 应该 根据以下参数计算所有出现次数:

  • 重复次数 - 会发生多少次
  • 工作日 - 一周中的哪一天发生
  • 开始日期 - 模式计算何时开始
  • 周期性 - 以周为单位的频率,即每周 1 次,每 2 周 2 次​​em>
  • 开始周 - 第一次出现的周数,反映开始日期列

这就是我存储模式的方式:

/*
    Pattern Table
*/
CREATE TABLE Pattern (
    [Id] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY
,   [Subject] [nvarchar](100) NULL
,   [RecurrenceCount] [int] NULL
,   [WeekDays] [varchar](max) NULL
,   [StartDate] [datetime] NULL
,   [EndDate] [datetime] NULL
,   [Periodicity] [int] NULL
,   [StartingWeek] [int] NULL

);

以下是我用来测试我的 CTE 的几个模式:

/*
    Pattern samples for test
*/
Insert into Pattern Values (N'Every 5 Weeks Fri, Sat, Sun', 72, 'Friday, Saturday, Sunday', N'2016-12-02', N'2016-12-02', 5, datepart(wk, N'2016-12-02'));
Insert into Pattern Values (N'Every 3 Weeks Tue, Wed, Thu', 20, 'Tuesday, Wednesday, Thursday', N'2016-11-01', N'2016-11-01', 3, datepart(wk, N'2016-11-01'));

考虑到一周的第一天星期一

,我开始数数
SET DATEFIRST 1

这是我用来运行此计算的 CTE:

/*
    Display Patterns
*/
select * from Pattern

DECLARE @mindate DATE       = (SELECT MIN(StartDate) FROM Pattern)
DECLARE @maxmindate DATE    = (SELECT MAX(StartDate) FROM Pattern)
DECLARE @maxcount INT       = (SELECT MAX(RecurrenceCount) FROM Pattern)
DECLARE @maxdate DATE       = DATEADD(WK, @maxcount + 10, @maxmindate)

/*
    CTE to generate required occurrences
*/
;With cteKeyDate As (
    Select 
        KeyStartDate = @MinDate, 
        KeyDOW = DateName(WEEKDAY, @MinDate), 
        KeyWeek = datepart(WK,@MinDate)
    Union All
    Select 
        KeyStartDate =  DateAdd(DD, 1, df.KeyStartDate) ,
        KeyDOW = DateName(WEEKDAY,DateAdd(DD, 1, df.KeyStartDate)), 
        KeyWeek= DatePart(WK,DateAdd(DD, 1, df.KeyStartDate))
    From cteKeyDate DF
    Where DF.KeyStartDate <= @MaxDate
) 

SELECT 
    Id, KeyStartDate, KeyDow, KeyWeek, RowNr, OccNr = ROW_NUMBER() OVER (PARTITION BY Id ORDER BY StartDate) 
FROM
    (Select 
        A.Id
        ,A.StartDate
        ,A.EndDate
        ,Count = A.RecurrenceCount
        ,Days = A.WeekDays
        ,Every = A.Periodicity              
        ,KeyStartDate = CASE
        /* 
            if no periodicity (1) then it is sequential
            if periodicity, first week doesn't apply (MIN KeyWeek)
        */
        WHEN A.Periodicity = 1 
            OR ( Periodicity <> 1 AND (SELECT MIN(C.StartingWeek) FROM Pattern AS C WHERE C.Id = A.Id) = KeyWeek )
            THEN KeyStartDate
        /* Otherwise formula ADD WEEKS => Current Week Min Week */
        ELSE
            DATEADD( WK, ((A.Periodicity - 1) * ( KeyWeek - (SELECT MIN(C.StartingWeek) FROM Pattern AS C WHERE C.Id = A.Id) ) ) , KeyStartDate )
        END
        ,KeyDow
        ,KeyWeek
        ,RowNr = Row_Number() over (Partition By A.Id Order By B.KeyStartDate)
        ,Periodicity = A.Periodicity
    from 
        Pattern A
        Join cteKeyDate B on B.KeyStartDate >= DATEADD(DAY, -1, A.StartDate) and Charindex(KeyDOW, A.WeekDays) > 0
    ) Final                 
Where 
    RowNr <= Count AND Id = 1
Option (maxrecursion 32767)

现在,如果我再次测试我的模式,例如第一个模式,我会得到这个结果,它在明年发生时会出现错误。 RowNr 15 是错误的,因为它应该发生在 4 月 23 日(周日)而不是下周。

Id  KeyStartDate    KeyDow      KeyWeek RowNr   OccNr
1   02.12.2016      Friday      49      1       1
2   03.12.2016      Saturday    49      2       2
3   04.12.2016      Sunday      49      3       3
4   06.01.2017      Friday      50      4       4
5   07.01.2017      Saturday    50      5       5
6   08.01.2017      Sunday      50      6       6
7   10.02.2017      Friday      51      7       7
8   11.02.2017      Saturday    51      8       8
9   12.02.2017      Sunday      51      9       9
10  17.03.2017      Friday      52      10      10
11  18.03.2017      Saturday    52      11      11
12  19.03.2017      Sunday      52      12      12
13  21.04.2017      Friday      53      13      13
14  22.04.2017      Saturday    53      14      14
15  28.04.2013      Sunday      1       15      15
16  31.05.2013      Friday      2       16      16
17  01.06.2013      Saturday    2       17      17

虽然第二个模式计算得很好。我认为当模式跨年并且在 SQL 中将周数重置为 0 时,我的逻辑存在问题,但我找不到解决方案,我现在挣扎了几天。

您可以使用示例here 执行代码。

【问题讨论】:

  • 作为参考,您不需要在您的ROW_NUMBER 中使用PARTITION BY,并且不要使用AB 部署代码, C 表别名。
  • @iamdave 关于别名,我不使用 A、B、C,但 A 代表“活动”,因此需要一个众所周知的别名 About PARTITION BY,否则当查询在多个模式中运行时某些 SQL 服务器(如 2008),行排序不正确
  • 您能否解释一下 CTE 正在尝试做什么,您要在这里完成什么,这将使我们更好地理解您的代码,目前您的开始日期和结束日期是相同的模式表,你能试着告诉我们你想在这里完成什么..
  • 我认为@Surendra 很清楚,开始日期和结束日期与约会有关,所以他们不计算在这里,除了 MIN(StartDate) 是第一次发生的时间,这是给CTE的开始也是。我还在文中添加了额外的解释

标签: sql-server tsql common-table-expression datepart


【解决方案1】:

花点时间在这上面。您使用的计算有缺陷。除非您的问题中没有说明特殊规则,否则我不明白为什么有些日期很特殊。我更喜欢使用变量表。

    /*
        Pattern Table
    */
    DECLARE @Pattern TABLE(
        [Id] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY
        ,[Subject] [nvarchar](100) NULL
        ,[RecurrenceCount] [int] NULL
        ,[WeekDays] [varchar](max) NULL
        ,[StartDate] [datetime] NULL
        ,[EndDate] [datetime] NULL
        ,[Periodicity] [int] NULL
        ,[StartingWeek] [int] NULL
    );

    /*
        Populate with values based on Recurreance and Startdate. The startdate will give the start week, which make the start week obsolete.
    */
    DECLARE @PreferredDate TABLE(
        [Id] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY
        ,[PreferredDate] [datetime] NULL
        ,[PreferredWeek] [int] NULL
        ,[PreferredYear] [int] NULL
    )

始终检索 datefirst 的当前设置非常重要。如果他们使用其他设置,您将破坏其他人的计算。出于显而易见的原因,我还添加了模式 ID。

    DECLARE @DateFirst int = @@dateFirst --DATEFIRST is a global setting

    DECLARE @mindate DATE       = (SELECT MIN(StartDate) FROM @Pattern WHERE id=@PreferredSubjectID)
    DECLARE @maxmindate DATE    = (SELECT MAX(StartDate) FROM @Pattern WHERE Id=@PreferredSubjectID)
    DECLARE @maxcount INT       = (SELECT MAX(RecurrenceCount) FROM @Pattern WHERE Id=@PreferredSubjectID) 
    DECLARE @maxdate DATE       = DATEADD(WK, @maxcount + 50, @maxmindate)

    SET DATEFIRST 1

    DECLARE @PreferredSubjectID int  = 1

@preferreddate 表使用以下内容填充:

    /*
        CTE to generate required preferred dates
    */
    ;With ctePreferredDate AS (
        Select  PreferredDate = @MinDate, PreferredWeek = DATEPART(WK, @MinDate), PreferredYear = DATEPART(YYYY, @MinDate)
        Union All
        SELECT  PreferredDate = DATEADD(WK,(SELECT Periodicity FROM @Pattern WHERE Id=@PreferredSubjectID), PreferredDate)
                ,PreferredWeek = DATEPART(WK,DATEADD(WK,(SELECT Periodicity FROM @Pattern WHERE Id=@PreferredSubjectID), PreferredDate))
                ,PreferredYear = DATEPART(yyyy,DATEADD(WK,(SELECT Periodicity FROM @Pattern WHERE Id=@PreferredSubjectID), PreferredDate))
        From ctePreferredDate pFD
        Where pFD.PreferredDate <= @MaxDate

    )
    INSERT INTO @PreferredDate (PreferredDate, PreferredWeek, PreferredYear)
    SELECT PreferredDate, PreferredWeek, PreferredYear
    FROM ctePreferredDate

使用以下内容填充最终的 CTE 表:

    /*
        CTE to generate required occurrences
    */
    ;With cteKeyDate As (
        Select  KeyStartDate = @MinDate 
                ,KeyDOW = DateName(WEEKDAY, @MinDate)
                ,KeyWeek = datepart(WK,@MinDate)
                ,id = @PreferredSubjectID
                ,KeyOccurrance = @maxcount
        Union All
        Select  KeyStartDate =  DateAdd(DD, 1, df.KeyStartDate)
                ,KeyDOW = DateName(WEEKDAY,DateAdd(DD, 1, df.KeyStartDate))
                ,KeyWeek= DatePart(WK,DateAdd(DD, 1, df.KeyStartDate))
                ,id=@PreferredSubjectID
                ,KeyOccurrance = @maxcount
        From cteKeyDate DF
        Where DF.KeyStartDate <= @MaxDate
    ) 
    SELECT  StartDate
            ,[DayOfWeek]
            ,[Week]
            ,OccNr = ROW_NUMBER() OVER         (PARTITION BY Id ORDER BY StartDate) 
    FROM
        (
        SELECT  cte.KeyStartDate AS StartDate
                ,cte.KeyDOW AS [DayOfWeek]
                ,cte.KeyWeek AS [Week]
                ,cte.id
                ,cte.KeyOccurrance AS Occurrance
                ,RowNr = ROW_NUMBER() OVER         (PARTITION BY KeyOccurrance ORDER BY KeyStartDate)
        FROM    cteKeyDate cte
                INNER JOIN
                @PreferredDate pfd
                    ON  cte.KeyWeek = pfd.PreferredWeek
                        AND YEAR(cte.KeyStartDate) = pfd.PreferredYear
        WHERE cte.KeyDOW IN (SELECT LTRIM(RTRIM(Item)) FROM fn_SplitString((SELECT weekdays from @Pattern WHERE Id=1),','))
        )cte
    WHERE   cte.RowNr <= cte.Occurrance
    ORDER BY cte.StartDate
    Option (maxrecursion 32767)

    SET DATEFIRST @DateFirst --Quite important

结果

2016/12/02  Friday  49  1
2016/12/03  Saturday    49  2
2016/12/04  Sunday  49  3
2017/01/06  Friday  2   4
2017/01/07  Saturday    2   5
2017/01/08  Sunday  2   6
2017/02/10  Friday  7   7
2017/02/11  Saturday    7   8
2017/02/12  Sunday  7   9
2017/03/17  Friday  12  10
2017/03/18  Saturday    12  11
2017/03/19  Sunday  12  12
2017/04/21  Friday  17  13
2017/04/22  Saturday    17  14
2017/04/23  Sunday  17  15
2017/05/26  Friday  22  16
2017/05/27  Saturday    22  17
2017/05/28  Sunday  22  18
2017/06/30  Friday  27  19
2017/07/01  Saturday    27  20
2017/07/02  Sunday  27  21
2017/08/04  Friday  32  22
2017/08/05  Saturday    32  23
2017/08/06  Sunday  32  24
2017/09/08  Friday  37  25
2017/09/09  Saturday    37  26
2017/09/10  Sunday  37  27
2017/10/13  Friday  42  28
2017/10/14  Saturday    42  29
2017/10/15  Sunday  42  30
2017/11/17  Friday  47  31
2017/11/18  Saturday    47  32
2017/11/19  Sunday  47  33
2017/12/22  Friday  52  34
2017/12/23  Saturday    52  35
2017/12/24  Sunday  52  36
2018/01/26  Friday  4   37
2018/01/27  Saturday    4   38
2018/01/28  Sunday  4   39
2018/03/02  Friday  9   40
2018/03/03  Saturday    9   41
2018/03/04  Sunday  9   42
2018/04/06  Friday  14  43
2018/04/07  Saturday    14  44
2018/04/08  Sunday  14  45
2018/05/11  Friday  19  46
2018/05/12  Saturday    19  47
2018/05/13  Sunday  19  48
2018/06/15  Friday  24  49
2018/06/16  Saturday    24  50
2018/06/17  Sunday  24  51
2018/07/20  Friday  29  52
2018/07/21  Saturday    29  53
2018/07/22  Sunday  29  54
2018/08/24  Friday  34  55
2018/08/25  Saturday    34  56
2018/08/26  Sunday  34  57
2018/09/28  Friday  39  58
2018/09/29  Saturday    39  59
2018/09/30  Sunday  39  60
2018/11/02  Friday  44  61
2018/11/03  Saturday    44  62
2018/11/04  Sunday  44  63
2018/12/07  Friday  49  64
2018/12/08  Saturday    49  65
2018/12/09  Sunday  49  66
2019/01/11  Friday  2   67
2019/01/12  Saturday    2   68
2019/01/13  Sunday  2   69
2019/02/15  Friday  7   70
2019/02/16  Saturday    7   71
2019/02/17  Sunday  7   72

分割字符串函数:

ALTER FUNCTION [dbo].[fn_SplitString](
    @InputStr   varchar(Max),
    @Seperator  varchar(10))
RETURNS @OutStrings TABLE (ItemNo int identity(1,1), Item varchar(256))

AS
BEGIN

    DECLARE @Str varchar(2000),
            @Poz int, @cnt int

    --DECLARE @OutStrings TABLE (Item varchar(2000))

    SELECT @Poz = CHARINDEX (@Seperator, @InputStr), @cnt = 0
    WHILE @Poz > 0 AND @cnt <= 10000
    BEGIN
        SELECT @Str = SubString(@InputStr, 1, @Poz - 1)
        INSERT INTO @OutStrings(Item) VALUES(@Str)

        SELECT @InputStr = Right(@Inputstr, Len(@InputStr) - (len(@Str) + len(@Seperator)))
        SELECT @Poz = CHARINDEX (@Seperator, @InputStr), @cnt = @cnt + 1
    END
    IF @InputStr <> ''
    BEGIN
        INSERT INTO @OutStrings(Item) VALUES(@InputStr)
    END

    RETURN
END

【讨论】:

  • 很好,今晚给我玩,我会回复的,谢谢你的努力;-)
  • 你做到了@danie-schoeman,我花了一段时间来测试它并重构我当前的视图,所以有了你的解决方案,我现在使用的是表函数而不是视图,但我必须说它有效,它很快并且涵盖了所有模式。谢谢!!
  • 很高兴我能提供帮助。
  • 最后一个问题。在您的示例中,您通过 PatternId 运行所有内容。现在,假设我的表 Patterns 中有两个模式,并且我想从这两个模式中返回所有出现的事件,我应该如何解决这个问题?我应该查询每个 Pattern 行的函数并在最后将所有行聚合在一起吗?
  • 我愿意。您将更好地控制输出。如果您要处理许多几乎相似的模式,它可能会变得相当复杂。
【解决方案2】:

您遇到该问题的原因是您使用 datepart 来获取周数,因此一旦年份更改,datepart 就会回到 1。这应该更改... 我做了这个更改,日期现在按顺序排列,检查一下。 顺便说一句,更改是第一个 CTE。

/*
    Display Patterns
*/
select * from Pattern

DECLARE @mindate DATE       = (SELECT MIN(StartDate) FROM Pattern)
DECLARE @maxmindate DATE    = (SELECT MAX(StartDate) FROM Pattern)
DECLARE @maxcount INT       = (SELECT MAX(RecurrenceCount) FROM Pattern)
DECLARE @maxdate DATE       = DATEADD(WK, @maxcount + 10, @maxmindate)

declare @minWeekPart INT = DATEPART(WK,@MinDate)
/*
    CTE to generate required occurrences
*/
;With cteKeyDate As (
    Select 
        KeyStartDate = @MinDate, 
        KeyDOW = DateName(WEEKDAY, @MinDate), 
        KeyWeek = @minWeekPart
    Union All
    Select 
        KeyStartDate =  DateAdd(DD, 1, df.KeyStartDate) ,
        KeyDOW = DateName(WEEKDAY,DateAdd(DD, 1, df.KeyStartDate)), 
        KeyWeek= @minWeekPart + datediff(WK,@MinDate,DateAdd(DD, 1, df.KeyStartDate))
    From cteKeyDate DF
    Where DF.KeyStartDate <= @MaxDate
) 
--select * from cteKeyDate
--    order by 1
--Option (maxrecursion 32767)


SELECT 
    Id, KeyStartDate, KeyDow, KeyWeek, RowNr, OccNr = ROW_NUMBER() OVER (PARTITION BY Id ORDER BY StartDate) 
FROM
    (Select 
        A.Id
        ,A.StartDate
        ,A.EndDate
        ,Count = A.RecurrenceCount
        ,Days = A.WeekDays
        ,Every = A.Periodicity              
        ,KeyStartDate = CASE
        /* 
            if no periodicity (1) then it is sequential
            if periodicity, first week doesn't apply (MIN KeyWeek)
        */
        WHEN A.Periodicity = 1 
            OR ( Periodicity <> 1 AND (SELECT MIN(C.StartingWeek) FROM Pattern AS C WHERE C.Id = A.Id) = KeyWeek )
            THEN KeyStartDate
        /* Otherwise formula ADD WEEKS => Current Week Min Week */
        ELSE
            DATEADD( WK, ((A.Periodicity - 1) * ( KeyWeek - (SELECT MIN(C.StartingWeek) FROM Pattern AS C WHERE C.Id = A.Id) ) ) , KeyStartDate )
        END
        ,KeyDow
        ,KeyWeek
        ,RowNr = Row_Number() over (Partition By A.Id Order By B.KeyStartDate)
        ,Periodicity = A.Periodicity
    from 
        Pattern A
        Join cteKeyDate B on B.KeyStartDate >= DATEADD(DAY, -1, A.StartDate) and Charindex(KeyDOW, A.WeekDays) > 0
    ) Final                 
Where 
    RowNr <= Count AND Id = 1
    order by 2 
Option (maxrecursion 32767)

我确实在 rextester 中运行过它,你可以在这里找到它 --> http://rextester.com/GWEY37271

【讨论】:

  • 嗨,实际上我测试过,它重现了同样的错误,我做了一个截图。第 53 周后,您的脚本通过增加周数起作用,但日期计算错误:snag.gy/MoNBxw.jpg
  • 你的截图对我来说不是很清楚,现在 16 日是 2017 年 5 月 26 日是什么错误,这不是正确的,它是星期天之后的下一个星期五......它完全适合. 哪一行有误.. 你能不能让我... 哦顺便说一下,我给出的 rexter 链接是错误的,它指向 .yours 而不是我的...
  • 第 15 行表示 2017 年 5 月 21 日,但应该是 2017 年 4 月 23 日
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-01-02
  • 2020-04-11
  • 1970-01-01
  • 2019-02-10
  • 1970-01-01
  • 2013-02-22
  • 2019-07-06
相关资源
最近更新 更多