【问题标题】:Splitting dates into intervals using Start Date and End Date使用开始日期和结束日期将日期拆分为间隔
【发布时间】:2018-11-15 07:53:06
【问题描述】:

我有需要将给定日期范围分成每月间隔的情况。

例如输入如下:

StartDate   EndDate
2018-01-21  2018-01-29
2018-01-30  2018-02-23
2018-02-24  2018-03-31
2018-04-01  2018-08-16
2018-08-17  2018-12-31

预期的输出应该如下所示:

StartDate   EndDate
2018-01-21  2018-01-29
2018-01-30  2018-01-31
2018-02-01  2018-02-23
2018-02-24  2018-02-28
2018-03-01  2018-03-31
2018-04-01  2018-04-30
2018-05-01  2018-05-31
2018-06-01  2018-06-30
2018-07-01  2018-07-31
2018-08-01  2018-08-16
2018-08-17  2018-08-31
2018-09-01  2018-09-30
2018-10-01  2018-10-31
2018-11-01  2018-11-30
2018-12-01  2018-12-31

以下是示例数据。

CREATE TABLE #Dates
(
    StartDate DATE,
    EndDate DATE
);


INSERT INTO #Dates
(
    StartDate,
    EndDate
)
VALUES
('2018-01-21', '2018-01-29'),
('2018-01-30', '2018-02-23'),
('2018-02-24', '2018-03-31'),
('2018-04-01', '2018-08-16'),
('2018-08-17', '2018-12-31');

【问题讨论】:

  • 很高兴您正确地发布了示例数据,但您也应该发布您迄今为止尝试过的内容。更多详情请阅读How to Ask
  • 另外,如果您在StartDate 中有一个值,即月末(如2018-01-31),该怎么办?
  • 如果 StartDate 是 2018-01-31,那么输出应该是 StartDate - 2018-01-31 EndDate - 2018-01-31
  • 这可以通过日历表轻松解决。这是 Aaron Bertrand 在how to create one. 上的一篇(又一篇)精彩文章

标签: sql sql-server tsql datetime sql-server-2008-r2


【解决方案1】:

您可以使用递归 CTE。基本思想是从第一个日期2018-01-21 开始,并建立一个所有月份的开始和结束日期列表,直到最后一个日期2018-12-31。然后与您的数据进行内部连接,并在必要时限定日期。

DECLARE @Dates TABLE (StartDate DATE, EndDate DATE);
INSERT INTO @Dates (StartDate, EndDate) VALUES
('2018-01-21', '2018-01-29'),
('2018-01-30', '2018-02-23'),
('2018-02-24', '2018-03-31'),
('2018-04-01', '2018-08-16'),
('2018-08-17', '2018-12-31');

WITH minmax AS (
    -- clamp min(start date) to 1st day of that month
    SELECT DATEADD(MONTH, DATEDIFF(MONTH, CAST('00010101' AS DATE), MIN(StartDate)), CAST('00010101' AS DATE)) AS mindate, MAX(EndDate) AS maxdate
    FROM @Dates
), months AS (
    -- calculate first and last day of each month
    -- e.g. for February 2018 it'll return 2018-02-01 and 2018-02-28
    SELECT mindate AS date01, DATEADD(DAY, -1, DATEADD(MONTH, 1, mindate)) AS date31, maxdate
    FROM minmax
    UNION ALL
    SELECT DATEADD(MONTH, 1, prev.date01), DATEADD(DAY, -1, DATEADD(MONTH, 2, prev.date01)), maxdate
    FROM months AS prev
    WHERE prev.date31 < maxdate
)
SELECT
    -- clamp start and end date to first and last day of corresponding month
    CASE WHEN StartDate < date01 THEN date01 ELSE StartDate END,
    CASE WHEN EndDate > date31 THEN date31 ELSE EndDate END
FROM months
INNER JOIN @Dates ON date31 >= StartDate AND EndDate >= date01

如果 rCTE 不是一个选项,您始终可以使用数字表或日期表加入(上述想法仍然适用)。

【讨论】:

  • EOMONTH是2012版引入的,我觉得IIF也是。
  • @Zohar 更改为独家结束日期,但无法验证它是否在 2008 年运行。
  • 我也不能....我知道的所有小提琴网站都只支持 2012 或更高版本,而我正在使用 2016... 我猜 OP 可以测试,不过 :- )
  • 看看我的尝试,它没有递归 cte
【解决方案2】:

您可以使用 Master..spt_values 表交叉应用以获取 StartDate 和 EndDate 之间每个月的一行。

SELECT * 
into #dates
FROM (values 
('2018-01-21', '2018-01-29')
,('2018-01-30', '2018-02-23')
,('2018-02-24', '2018-03-31')
,('2018-04-01', '2018-08-16')
,('2018-08-17', '2018-12-31')
)d(StartDate  , EndDate)



SELECT
    SplitStart as StartDate 
    ,case when enddate < SplitEnd then enddate else SplitEnd end as EndDate
FROM  #dates d
cross apply (
    SELECT 
        cast(dateadd(mm, number, dateadd(dd, (-datepart(dd, d.startdate) +1) * isnull((number / nullif(number, 0)), 0), d.startdate)) as date) as SplitStart
        ,cast(dateadd(dd, -datepart(dd, dateadd(mm, number+1, startdate)), dateadd(mm, number+1, startdate)) as date) as SplitEnd
    FROM 
    master..spt_values 
    where type = 'p' 
      and number between 0 and (((year(enddate) - year(startdate)) * 12) +  month(enddate) - month(startdate))   
) s

drop table #dates

【讨论】:

  • 如果一年中有日期怎么办,例如'2018-12-10', '2019-01-10'?
  • 好点。更新了我的答案以适应跨年日期范围
【解决方案3】:

以下也应该有效

  1. 首先,我将 startdates 和 enddates 放入 cte-block 数据的单个列中。
  2. 在 som_eom 块中,我为所有 12 个月创建了 start_of_month 和 end_of_month。
  3. 我将步骤 1 和 2 合并到 curated_set 中
  4. 我创建了按日期列排序的 curated_set
  5. 最后我拒绝了不需要的记录,在我的过滤子句中不是 in('som','StartDate')
with data
   as (select *
         from dates
        unpivot(x for y in(startdate,enddate))t
       )
    ,som_eom
      as (select top 12
                 cast('2018-'+cast(row_number() over(order by (select null)) as varchar(2))+'-01' as date) as som
                 ,dateadd(dd
                          ,-1
                           ,dateadd(mm
                                    ,1
                                    ,cast('2018-'+cast(row_number() over(order by (select null)) as varchar(2))+'-01' as date)
                                    )
                           ) as eom
                from information_schema.tables
            )
      ,curated_set
        as(select *
             from data
            union all
            select *
              from som_eom
            unpivot(x for y in(som,eom))t
            )
       ,curated_data
         as(select x
                  ,y
                  ,lag(x) over(order by x) as prev_val
              from curated_set
             )
select prev_val as st_dt,x as end_dt
       ,y  
  from curated_Data
where y not in('som','StartDate')

【讨论】:

    【解决方案4】:

    从初始 StartDate 开始并计算月末,或者如果 EndDate 在同一个月内,则直接使用它。 使用新计算的 EndDate+1 作为 StartDate 进行递归,重复计算。

    WITH cte AS 
     ( SELECT StartDate, -- initial start date
          CASE WHEN EndDate < DATEADD(DAY,-1,DATEADD(MONTH, DATEDIFF(MONTH,0,StartDate)+1,0))
               THEN EndDate
               ELSE           DATEADD(DAY,-1,DATEADD(MONTH, DATEDIFF(MONTH,0,StartDate)+1,0))
          END AS newEnd, -- LEAST(end of current month, EndDate)
          EndDate
       FROM #Dates
    
       UNION ALL
    
       SELECT dateadd(DAY,1,newEnd), -- previous end + 1 day, i.e. 1st of current month
          CASE WHEN EndDate <= DATEADD(DAY,-1,DATEADD(MONTH, DATEDIFF(MONTH,0,StartDate)+2,0)) 
               THEN EndDate 
               ELSE            DATEADD(DAY,-1,DATEADD(MONTH, DATEDIFF(MONTH,0,StartDate)+2,0)) 
          END, -- LEAST(end of next month, EndDate)
          EndDate
       FROM cte
       WHERE newEnd < EndDate 
     )
    SELECT StartDate, newEnd 
    FROM cte
    

    【讨论】: