【问题标题】:SQL calculate date segments within calendar yearSQL计算日历年内的日期段
【发布时间】:2015-10-19 19:21:07
【问题描述】:

我需要的是在 SQL 中给定这样的表来计算日历年内缺失的时间段:

DatesTable
|ID|DateStart   |DateEnd   |
 1  NULL         NULL
 2  2015-1-1     2015-12-31
 3  2015-3-1     2015-12-31
 4  2015-1-1     2015-9-30
 5  2015-1-1     2015-3-31
 5  2015-6-1     2015-12-31
 6  2015-3-1     2015-6-30
 6  2015-7-1     2015-10-31

预期回报为:

 1  2015-1-1     2015-12-31
 3  2015-1-1     2015-2-28
 4  2015-10-1    2015-12-31
 5  2015-4-1     2015-5-31
 6  2015-1-1     2015-2-28
 6  2015-11-1    2015-12-31

本质上是工作块。我需要展示的是日历年中没有工作的部分。所以对于 ID = 3,他从 3 月 1 日一直工作到今年剩下的时间。但他从 1 月 1 日到 2 月 28 日都没有工作。这就是我要找的。​​p>

【问题讨论】:

  • 能否解释一下记录 ID = 3 等结果的逻辑?
  • 这是 SQL Server 2008 的一个痛点。你能升级到 SQL Server 2012 吗?
  • 考虑完成。如果有帮助,我可以在 2012 年运行它。
  • 我认为您将不得不使用带有游标或表变量的存储过程,尽管您可以使用递归函数来做一些事情。

标签: sql date sql-server-2012


【解决方案1】:

您可以使用 SQL Server 2012+ 中提供的 LEADLAG 窗口函数来实现:

;WITH CTE AS (
   SELECT ID, 
          LAG(DateEnd) OVER (PARTITION BY ID ORDER BY DateEnd) AS PrevEnd,
          DateStart,
          DateEnd,
          LEAD(DateStart) OVER (PARTITION BY ID ORDER BY DateEnd) AS NextStart
   FROM DatesTable
)
SELECT ID, DateStart, DateEnd
FROM (
-- Get interval right before current [DateStart, DateEnd] interval
SELECT ID, 
       CASE 
          WHEN DateStart IS NULL THEN '20150101'
          WHEN DateStart > start THEN start
          ELSE NULL
       END AS DateStart,
       CASE 
          WHEN DateStart IS NULL THEN '20151231'
          WHEN DateStart > start THEN DATEADD(d, -1, DateStart)
          ELSE NULL
       END AS DateEnd
FROM CTE
CROSS APPLY (SELECT COALESCE(DATEADD(d, 1, PrevEnd), '20150101')) x(start)

-- If there is no next interval then get interval right after current 
-- [DateStart, DateEnd] interval (up-to end of year)
UNION ALL

SELECT ID, DATEADD(d, 1, DateEnd) AS DateStart, '20151231' AS DateEnd       
FROM CTE
WHERE DateStart IS NOT NULl    -- Do not re-examine [Null, Null] interval
      AND NextStart IS NULL    -- There is no next [DateStart, DateEnd] interval 
      AND DateEnd < '20151231' -- Current [DateStart, DateEnd] interval  
                               -- does not terminate on 31/12/2015
) AS t
WHERE t.DateStart IS NOT NULL
ORDER BY ID, DateStart

上述查询背后的想法很简单:对于每个[DateStart, DateEnd] 间隔,在它之前获取'not working' 间隔。如果当前间隔之后没有间隔,则还获得连续的'not working'间隔(如果有)。

另外请注意,如果DateStartNULL,那么DateStart 也是NULL 对于相同的ID

Demo here

【讨论】:

    【解决方案2】:

    如果您的数据不是太大,这种方法会奏效。它扩展了所有的天数和 ID,然后将它们重新分组:

    with d as (
          select cast('2015-01-01' as date)
          union all
          select dateadd(day, 1, d)
          from d
          where d < cast('2015-12-31' as date)
         ),
         td as (
          select *
          from d cross join
               (select distinct id from t) t
          where not exists (select 1
                            from t t2
                            where d.d between t2.startdate and t2.enddate
                           )
         )
    select id, min(d) as startdate, max(d) as enddate
    from (select td.*,
                 dateadd(day, - row_number() over (partition by id order by d), d) as grp
          from td
         ) td
    group by id, grp
    order by id, grp;
    

    另一种方法依赖于累积和以及在 SQL Server 2012+ 中更容易表达的类似功能。

    【讨论】:

    • 我正在运行 SQL Server 2012。我已更改与问题关联的标签。
    【解决方案3】:

    我认为有点简单的方法。

    基本上为所有工作块范围 (A) 创建一个日期列表。然后为每个 ID (B) 创建一个全年日期列表。然后从 B 中删除 A。将剩余的日期列表编译为每个 ID 的日期范围。

    DECLARE @startdate DATETIME, @enddate DATETIME
    SET @startdate = '2015-01-01'
    SET @enddate = '2015-12-31'
    
    --Build date ranges from remaining date list
    ;WITH dateRange(ID, dates, Grouping)
    AS
    (
        SELECT dt1.id, dt1.Dates, dt1.Dates + row_number() over (order by dt1.id asc, dt1.Dates desc) AS Grouping
        FROM 
        (
            --Remove (A) from (B)
            SELECT distinct dt.ID, tmp.Dates FROM DatesTable dt
            CROSS APPLY
            (
                --GET (B) here
                SELECT DATEADD(DAY, number, @startdate) [Dates]
                FROM master..spt_values
                WHERE type = 'P' AND DATEADD(DAY, number, @startdate) <= @enddate
            ) tmp
            left join
            (
                --GET (A) here
                SELECT DISTINCT T.Id,
                   D.Dates
                FROM DatesTable AS T
                INNER JOIN master..spt_values as N on N.number between 0 and datediff(day, T.DateStart, T.DateEnd)
                CROSS APPLY (select dateadd(day, N.number, T.DateStart)) as D(Dates)
                WHERE N.type ='P'
            ) dr
            ON dr.Id = dt.Id and dr.Dates = tmp.Dates
            WHERE dr.id is null
        ) dt1
    )
    SELECT ID, CAST(MIN(Dates) AS DATE) DateStart, CAST(MAX(Dates) AS DATE) DateEnd
    FROM dateRange
    GROUP BY ID, Grouping
    ORDER BY ID
    

    代码如下: http://sqlfiddle.com/#!3/f3615/1

    我希望这会有所帮助!

    【讨论】:

      猜你喜欢
      • 2022-06-16
      • 1970-01-01
      • 1970-01-01
      • 2018-12-09
      • 1970-01-01
      • 2023-02-22
      • 1970-01-01
      • 1970-01-01
      • 2018-04-24
      相关资源
      最近更新 更多