【问题标题】:SQL Server Date Range CalculationSQL Server 日期范围计算
【发布时间】:2018-09-19 13:28:05
【问题描述】:

我有如下表。

Workid  StartDate   EndDate
101     2012-03-01  2013-03-20
101     2013-03-21  2014-08-01
102     2014-01-01  2014-12-31
103     2012-01-01  2012-10-11
103     2012-10-12  2014-12-31
104     2012-01-01  2012-12-25
105     2012-01-01  2014-06-30
105     2014-07-01  2014-12-31

我需要验证给定年份的 workid 是否已完全占用。例如,如果我选择年份为 2012,那么输出应该如下所示。

WorkID  STATUS
101     Not Occupied
102     Not Occupied
103     Occupied
104     Not Occupied
105     Occupied

如果我选择年份为 2013,那么输出应该如下所示。

WorkID  STATUS
101     Occupied
102     Not Occupied
103     Occupied
104     Not Occupied
105     Occupied

有人可以帮忙吗?

下面是示例表脚本。

CREATE TABLE #temp
(
    Workid INT,
    StartDate DATE,
    EndDate DATE
)
go
INSERT INTO #temp
(
    Workid,
    StartDate,
    EndDate
)
VALUES
(101, '2012-03-01', '2013-03-20'),
(101, '2013-03-21', '2014-08-01'),
(102, '2014-01-01', '2014-12-31'),
(103, '2012-01-01', '2012-10-11'),
(103, '2012-10-12', '2014-12-31'),
(104, '2012-01-01', '2012-12-25'),
(105, '2012-01-01', '2014-06-30'),
(105, '2014-07-01', '2014-12-31')
;

【问题讨论】:

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


    【解决方案1】:

    您可以使用递归 cte 来查找每个 workid 的日期范围并进行比较,这也将解释工作中的空白(演示:http://rextester.com/NUEW6510

    DECLARE @year INT = 2013;
    WITH calendarSearch (Workid,StartDate, EndDate, StartDate2, EndDate2) AS
    (
        SELECT Workid,StartDate, EndDate, CAST('1900-01-01' AS DATE), CAST('1900-01-01' AS DATE)
        FROM #temp
        UNION ALL
        SELECT c.Workid, c.StartDate, t.EndDate, t.StartDate, c.EndDate
        FROM #temp t
        JOIN calendarSearch c
            ON c.Workid = t.Workid
            AND t.EndDate > c.EndDate
            AND t.StartDate <= DATEADD(DAY,1,c.EndDate)
    ), occupiedRange AS
    (
        SELECT Workid,StartDate, EndDate FROM calendarSearch c
            WHERE c.StartDate NOT IN (SELECT StartDate2 FROM calendarSearch WHERE calendarSearch.Workid = c.Workid)
                AND c.EndDate NOT IN (SELECT EndDate2 FROM calendarSearch WHERE calendarSearch.Workid = c.Workid)
    )
    
    SELECT Workid, 
    CASE WHEN EXISTS (SELECT 1 FROM occupiedRange WHERE Workid = o1.Workid and StartDate <= datefromparts(@year,1,1) AND EndDate >= DATEFROMPARTS(@year,12,31)) THEN 'Occupied' ELSE 'Not Occupied' END AS Status
    FROM occupiedRange o1
    GROUP BY o1.Workid
    ORDER BY Workid
    

    【讨论】:

      【解决方案2】:

      使用下面的查询

      select b.WORKID, (case when b.SD_COUNT > 0 and b.ED_COUNT > 0 then 'Occupied' else 'Not Occupied' end ) as STATUS from 
      (select a.Workid as WORKID, Sum(a.Status_StartDate) as SD_COUNT, Sum(a.Status_EndDate) as ED_COUNT from 
      (Select Workid, (case when StartDate <= '2012-01-01' then 1 else 0 end as Status_StartDate), (case when EndDate >= '2012-12-31' then 1 else 0 end) as Status_EndDate from tablename)a group by a.Workid)b;
      

      【讨论】:

      • 这个包含一些语法错误。再加上这个查询是无法阅读的,你可能想稍微分解一下逻辑。
      【解决方案3】:

      这花了我一段时间,但查询会起作用。可能有更好的方法,但正如我所说,这确实有效。测试一下,祝你好运!

      USE tempdb
      DECLARE @StartDate DATE
      DECLARE @EindDate DATE
      SET @StartDate = '1990-01-01'  -- << user input >> --
      SET @EindDate  = '2018-12-31'  -- << user input >> --
      
      IF OBJECT_ID ('TEMPDB..#Date') IS NOT NULL DROP TABLE #Date
      IF OBJECT_ID ('TEMPDB..#Date') IS NULL CREATE TABLE #Date (Date_ DATE)
      
      INSERT INTO #Date VALUES (@StartDate)
      
      WHILE @StartDate < @EindDate
      
      BEGIN
      INSERT INTO #Date
      SELECT DATEADD (DD, 1, @StartDate) AS Date
      SET @StartDate = DATEADD (DD, 1, @StartDate)
      END;
      
      
      SELECT * FROM #Date
      
      CREATE TABLE #temp
      (
          Workid INT,
          StartDate DATE,
          EndDate DATE
      )
      GO
      INSERT INTO #temp
      (
          Workid,
          StartDate,
          EndDate
      )
      VALUES
             (101, '2012-03-01', '2013-03-20'),
             (101, '2013-03-21', '2014-08-01'),
             (102, '2014-01-01', '2014-12-31'),
             (103, '2012-01-01', '2012-10-11'),
             (103, '2012-10-12', '2014-12-31'),
             (104, '2012-01-01', '2012-12-25'),
             (105, '2012-01-01', '2014-06-30'),
             (105, '2014-07-01', '2014-12-31');
      
      CREATE TABLE #WorkID (WorkID INT)
      INSERT INTO #WorkID VALUES (101), (102), (103), (104), (105)
      
      CREATE TABLE #Yrs (Yrs INT)
      INSERT INTO #Yrs VALUES (2012), (2013), (2014)
      
      SELECT * 
      INTO #ResultSet
      FROM #WorkID
      CROSS JOIN #Yrs;
      
      WITH CTE AS 
      (
          SELECT Workid, YEAR (Date_) AS Yr, COUNT(*) AS WorkedDays
          FROM #Date AS D
          INNER JOIN #temp AS T ON D.Date_ >= T.StartDate AND D.Date_ <= T.EndDate
          GROUP BY Workid, YEAR (Date_)
      )
      
      SELECT R.WorkID,
            Yrs AS Year_, 
            CASE WHEN Yr % 4 = 0 AND WorkedDays = 366 THEN 'Occupied'
                WHEN Yr % 4 <> 0 AND WorkedDays = 365 THEN 'Occupied'
                ELSE 'Not Occupied'
             END AS OccupiedStatus
      FROM #ResultSet AS R
      LEFT JOIN CTE AS C ON C.Workid = R.WorkID AND Yrs = Yr
      ORDER BY 2, 1
      

      【讨论】:

        【解决方案4】:

        您首先需要确定每个 WorkId 的最小 StartDate 和最大 EndDate:

               SELECT   [WorkId]
                      , MIN([StartDate]) AS [StartDate]
                      , MAX([EndDate]) AS [EndDate]
               FROM     [#temp]
               GROUP BY [WorkId]
        

        通过一些日期操作,您需要计算出您正在评估的年份的开始和结束日期,例如:

            DECLARE @Year INT = 2012;
        
            DECLARE @YearBegin DATE;
            DECLARE @YearEnd date;
        
            SET @YearBegin = DATEADD(yy, @Year - 1900, 0);
            SET @YearEnd = DATEADD(DAY, -1, DATEADD(yy, 1, @YearBegin));
        
            SELECT @YearBegin
                 , @YearEnd;
        

        然后我们可以根据@YearBegin 和@yearEnd 评估开始和结束日期以确定“完全占用”。

        这是假设每个 ID 的数据范围没有间隙:

            DECLARE @Year INT = 2012;
        
            DECLARE @YearBegin DATE;
            DECLARE @YearEnd DATE;
        
            SET @YearBegin = DATEADD(yy, @Year - 1900, 0);
            SET @YearEnd = DATEADD(DAY, -1, DATEADD(yy, 1, @YearBegin));
        
            SELECT [WorkId]
                 , CASE WHEN [fr].[StartDate] <= @YearBegin
                             AND [fr].[EndDate] >= @YearEnd THEN 'Occupied'
                        ELSE 'Not Occupied'
                   END AS [Status]
            FROM   (
                       SELECT   [WorkId]
                              , MIN([StartDate]) AS [StartDate]
                              , MAX([EndDate]) AS [EndDate]
                       FROM     [#temp]
                       GROUP BY [WorkId]
                   ) AS [fr];
        

        如果每个 ID 的日期范围和/或重叠的日期范围可能存在间隙,下面是一个递归 CTE 示例,可以解决这些情况:

            DECLARE @Year INT = 2013
                  , @TotalDayYear INT;
        
            --For the year we are evalutaing, how many days total in that year
            --We'll use for comparison later
            SET @TotalDayYear = DATEDIFF(
                                            DAY
                                          , CONCAT(@Year - 1, '-12-31')
                                          , CONCAT(@Year, '-12-31')
                                        );
        
        
            DECLARE @YearOccupied TABLE
                (
                    [WorkId] INT
                  , [YearOccupied] INT
                  , [CountDaysOccupied] INT
                );
        
        
            --Recursive CTE to get list of every day a particular ID occupied based on the date ranges.
            WITH [Cte]
            AS ( SELECT [WorkId]
                      , [StartDate]
                      , [EndDate]
                      , [StartDate] AS [DateOccupied]
                 FROM   [#temp]
                 UNION ALL
                 SELECT     [a].[WorkId]
                          , [a].[StartDate]
                          , [a].[EndDate]
                          , DATEADD(DAY, 1, [b].[DateOccupied]) AS [DateOccupied]
                 FROM       [#temp] [a]
                 INNER JOIN [Cte] [b]
                     ON [b].[WorkId] = [a].[WorkId]
                        AND [b].[StartDate] = [a].[StartDate]
                        AND [b].[EndDate] = [a].[EndDate]
                        AND [b].[DateOccupied] < [a].[EndDate] )
            --We'll insert into a table variable a count by year for each Id
            INSERT INTO @YearOccupied (
                                          [WorkId]
                                        , [YearOccupied]
                                        , [CountDaysOccupied]
                                      )
                        SELECT   [do].[WorkId]
                               , YEAR([DateOccupied]) AS [YearOccupied]
                               , COUNT(*) AS [DaysOccupiedCount]
                        FROM     (
                                     --Use a sub-query here to get a distinct Date Occupied
                                     --To account for overlapping data ranges
                                     SELECT DISTINCT [WorkId]
                                                   , [DateOccupied]
                                     FROM   [Cte]
                                 ) AS [do]
                        GROUP BY [do].[WorkId]
                               , YEAR([DateOccupied])
            OPTION ( MAXRECURSION 0 );
        
        
            SELECT          [a].[WorkId]
                          , @Year AS [YearOccupied]
                          --Coaleace as we may have a year an ID didn't occupy at all
                          --compared to the total days of the year to determine fully occupied or not
                          , CASE WHEN COALESCE([b].[CountDaysOccupied], 0) = @TotalDayYear THEN
                                     'Occupied'
                                 ELSE 'Not Occupied'
                            END AS [Status]
            FROM            (
                                --We could have a year we want data for that a particular id didn't occupy.
                                --We'll build a distinct list of ID and our evalution year for doing a left outer to
                                --accually see if the year was occupied.
                                SELECT DISTINCT [WorkId]
                                              , @Year AS [YearOccupied]
                                FROM   @YearOccupied
                            ) AS [a]
            LEFT OUTER JOIN @YearOccupied [b]
                ON [b].[WorkId] = [a].[WorkId]
                   AND [b].[YearOccupied] = @Year;
        

        【讨论】:

        • 此解决方案假定同一 ID 的所有时间跨度都是连续的
        • @iamdave 你是对的,我确实做了这个假设。我更新了我的答案。此外,考虑到可能存在重叠的日期范围。我添加了一个递归 CTE 示例,该示例将说明日期范围的间隙和/或重叠。
        猜你喜欢
        • 1970-01-01
        • 2013-12-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多