【问题标题】:Creating groups of consecutive days meeting a given criteria创建满足给定标准的连续天组
【发布时间】:2012-02-14 10:04:22
【问题描述】:

我在 SQL Server 中有以下数据结构的表:

ID  Date        Allocation
 1, 2012-01-01, 0
 2, 2012-01-02, 2
 3, 2012-01-03, 0
 4, 2012-01-04, 0
 5, 2012-01-05, 0
 6, 2012-01-06, 5

等等

我需要做的是获取分配 = 0 的所有连续天数,格式如下:

Start Date    End Date     DayCount
2012-01-01    2012-01-01   1
2012-01-03    2012-01-05   3

等等

是否可以在 SQL 中执行此操作,如果可以,如何操作?

【问题讨论】:

  • @istari 是表结构中的一列结束日期
  • 你试过使用游标吗?或者你不需要游标
  • 您的意思是“连续”,如“相隔一天”,还是“按日期排序时相邻”?即每个唯一日期是否在“日期”列中仅出现一次?

标签: sql sql-server-2008


【解决方案1】:

在这个答案中,我假设“id”字段在按递增日期排序时对行进行连续编号,就像在示例数据中一样。 (这样的列如果不存在可以创建)。

这是here 和here 描述的技术示例。

1) 在相邻的“id”值上将表连接到自身。这将相邻的行配对。选择“分配”字段已更改的行。将结果存储在临时表中,同时保留运行索引。

SET @idx = 0;
CREATE TEMPORARY TABLE boundaries
SELECT
   (@idx := @idx + 1) AS idx,
   a1.date AS prev_end,
   a2.date AS next_start,
   a1.allocation as allocation
FROM allocations a1
JOIN allocations a2
ON (a2.id = a1.id + 1)
WHERE a1.allocation != a2.allocation;

这为您提供了一个表格,其中每行包含“上一期的结束”、“下一期的开始”和“上一期的‘分配’值”:

+------+------------+------------+------------+
| idx  | prev_end   | next_start | allocation |
+------+------------+------------+------------+
|    1 | 2012-01-01 | 2012-01-02 |          0 |
|    2 | 2012-01-02 | 2012-01-03 |          2 |
|    3 | 2012-01-05 | 2012-01-06 |          0 |
+------+------------+------------+------------+

2)我们需要在同一行中每个周期的开始和结束,因此我们需要再次合并相邻的行。通过创建第二个临时表来做到这一点,例如 boundaries,但具有更大的 idx 字段 1:

+------+------------+------------+
| idx  | prev_end   | next_start |
+------+------------+------------+
|    2 | 2012-01-01 | 2012-01-02 |
|    3 | 2012-01-02 | 2012-01-03 |
|    4 | 2012-01-05 | 2012-01-06 |
+------+------------+------------+

现在加入idx 字段,我们就会得到答案:

SELECT
  boundaries2.next_start AS start,
  boundaries.prev_end AS end,
  allocation
FROM boundaries
JOIN boundaries2
USING(idx);

+------------+------------+------------+
| start      | end        | allocation |
+------------+------------+------------+
| 2012-01-02 | 2012-01-02 |          2 |
| 2012-01-03 | 2012-01-05 |          0 |
+------------+------------+------------+

** 请注意,此答案正确获取“内部”周期,但错过了两个“边缘”周期,其中分配 = 0 开头,分配 = 5 结尾。这些可以使用UNION 子句引入,但我想在没有复杂的情况下呈现核心思想。

【讨论】:

    【解决方案2】:

    以下是一种方法。该解决方案的要点是

    • 使用CTE 获取所有连续开始和结束日期的列表,其中Allocation = 0
    • 使用ROW_NUMBER 窗口函数根据开始日期和结束日期分配行号。
    • 仅选择ROW_NUMBERS 都等于 1 的记录。
    • 使用DATEDIFF计算DayCount

    SQL 语句

    ;WITH r AS (
      SELECT  StartDate = Date, EndDate = Date
      FROM    YourTable
      WHERE   Allocation = 0
      UNION ALL
      SELECT  r.StartDate, q.Date
      FROM    r
              INNER JOIN YourTable q ON DATEDIFF(dd, r.EndDate, q.Date) = 1
      WHERE   q.Allocation = 0          
    )
    SELECT  [Start Date] = s.StartDate
            , [End Date ] = s.EndDate
            , [DayCount] = DATEDIFF(dd, s.StartDate, s.EndDate) + 1
    FROM    (
              SELECT  *
                      , rn1 = ROW_NUMBER() OVER (PARTITION BY StartDate ORDER BY EndDate DESC)
                      , rn2 = ROW_NUMBER() OVER (PARTITION BY EndDate ORDER BY StartDate ASC)
              FROM    r          
            ) s
    WHERE   s.rn1 = 1
            AND s.rn2 = 1
    OPTION  (MAXRECURSION 0)
    

    测试脚本

    ;WITH q (ID, Date, Allocation) AS (
      SELECT * FROM (VALUES
        (1, '2012-01-01', 0)
        , (2, '2012-01-02', 2)
        , (3, '2012-01-03', 0)
        , (4, '2012-01-04', 0)
        , (5, '2012-01-05', 0)
        , (6, '2012-01-06', 5)
      ) a (a, b, c)
    )
    , r AS (
      SELECT  StartDate = Date, EndDate = Date
      FROM    q
      WHERE   Allocation = 0
      UNION ALL
      SELECT  r.StartDate, q.Date
      FROM    r
              INNER JOIN q ON DATEDIFF(dd, r.EndDate, q.Date) = 1
      WHERE   q.Allocation = 0          
    )
    SELECT  s.StartDate, s.EndDate, DATEDIFF(dd, s.StartDate, s.EndDate) + 1
    FROM    (
              SELECT  *
                      , rn1 = ROW_NUMBER() OVER (PARTITION BY StartDate ORDER BY EndDate DESC)
                      , rn2 = ROW_NUMBER() OVER (PARTITION BY EndDate ORDER BY StartDate ASC)
              FROM    r          
            ) s
    WHERE   s.rn1 = 1
            AND s.rn2 = 1
    OPTION  (MAXRECURSION 0)
    

    【讨论】:

    • @Istari - 我已经修改了一个 maxrecursion 选项来修复错误消息。
    【解决方案3】:

    使用 CTE 但没有 ROW_NUMBER() 的替代方式,

    样本数据:

    if object_id('tempdb..#tab') is not null
        drop table #tab
    
    create table #tab (id int, date datetime, allocation int)
    
    insert into #tab
    select 1, '2012-01-01', 0 union
    select 2, '2012-01-02', 2 union
    select 3, '2012-01-03', 0 union
    select 4, '2012-01-04', 0 union
    select 5, '2012-01-05', 0 union
    select 6, '2012-01-06', 5 union
    select 7, '2012-01-07', 0 union
    select 8, '2012-01-08', 5 union
    select 9, '2012-01-09', 0 union
    select 10, '2012-01-10', 0
    

    查询:

    ;with cte(s_id, e_id, b_id) as (
        select s.id, e.id, b.id
        from #tab s
        left join #tab e on dateadd(dd, 1, s.date) = e.date and e.allocation = 0
        left join #tab b on dateadd(dd, -1, s.date) = b.date and b.allocation = 0
        where s.allocation = 0
    )
    select ts.date as [start date], te.date as [end date], count(*) as [day count] from (
        select c1.s_id as s, (
            select min(s_id) from cte c2 
            where c2.e_id is null and c2.s_id >= c1.s_id
        ) as e
        from cte c1
        where b_id is null
    ) t
    join #tab t1 on t1.id between t.s and t.e and t1.allocation = 0
    join #tab ts on ts.id = t.s
    join #tab te on te.id = t.e
    group by t.s, t.e, ts.date, te.date
    

    Live example at data.SE.

    【讨论】:

      【解决方案4】:

      使用此示例数据:

      CREATE TABLE MyTable (ID INT, Date DATETIME, Allocation INT);
      INSERT INTO MyTable VALUES (1, {d '2012-01-01'}, 0);
      INSERT INTO MyTable VALUES (2, {d '2012-01-02'}, 2);
      INSERT INTO MyTable VALUES (3, {d '2012-01-03'}, 0);
      INSERT INTO MyTable VALUES (4, {d '2012-01-04'}, 0);
      INSERT INTO MyTable VALUES (5, {d '2012-01-05'}, 0);
      INSERT INTO MyTable VALUES (6, {d '2012-01-06'}, 5);
      GO
      

      试试这个:

      WITH DateGroups (ID, Date, Allocation, SeedID) AS (
          SELECT MyTable.ID, MyTable.Date, MyTable.Allocation, MyTable.ID
            FROM MyTable
            LEFT JOIN MyTable Prev ON Prev.Date = DATEADD(d, -1, MyTable.Date) 
                                  AND Prev.Allocation = 0
           WHERE Prev.ID IS NULL
             AND MyTable.Allocation = 0
          UNION ALL
          SELECT MyTable.ID, MyTable.Date, MyTable.Allocation, DateGroups.SeedID
            FROM MyTable
            JOIN DateGroups ON MyTable.Date = DATEADD(d, 1, DateGroups.Date)
           WHERE MyTable.Allocation = 0
      
      ), StartDates (ID, StartDate, DayCount) AS (
          SELECT SeedID, MIN(Date), COUNT(ID)
            FROM DateGroups
           GROUP BY SeedID
      
      ), EndDates (ID, EndDate) AS (
          SELECT SeedID, MAX(Date)
            FROM DateGroups
           GROUP BY SeedID
      
      )
      SELECT StartDates.StartDate, EndDates.EndDate, StartDates.DayCount
        FROM StartDates
        JOIN EndDates ON StartDates.ID = EndDates.ID;
      

      查询的第一部分是一个递归 SELECT,它由分配 = 0 且其前一天不存在或分配为 0 的所有行锚定。这有效地返回 ID:1 和 3这是您要返回的时间段的开始日期。

      同一查询的递归部分从锚行开始,并查找所有后续日期也具有分配 = 0。SeedID 通过所有迭代跟踪锚定 ID。

      目前的结果是这样的:

      ID          Date                    Allocation  SeedID
      ----------- ----------------------- ----------- -----------
      1           2012-01-01 00:00:00.000 0           1
      3           2012-01-03 00:00:00.000 0           3
      4           2012-01-04 00:00:00.000 0           3
      5           2012-01-05 00:00:00.000 0           3
      

      下一个子查询使用简单的 GROUP BY 过滤掉每个 SeedID 的所有开始日期,并计算天数。

      最后一个子查询对结束日期做同样的事情,但这次不需要天数,因为我们已经有了这个。

      最终的 SELECT 查询将这两者结合起来,组合开始日期和结束日期,并将它们与天数一起返回。

      【讨论】:

        【解决方案5】:

        试试看,如果它适合你 在这里,您的 DATE 的 SDATE 与您的表相同。

        SELECT SDATE,
        CASE WHEN (SELECT COUNT(*)-1 FROM TABLE1 WHERE ID BETWEEN TBL1.ID AND (SELECT MIN(ID) FROM TABLE1 WHERE ID > TBL1.ID AND ALLOCATION!=0)) >0 THEN(
        CASE WHEN (SELECT SDATE FROM TABLE1 WHERE ID =(SELECT MAX(ID) FROM TABLE1 WHERE ID >TBL1.ID AND ID<(SELECT MIN(ID) FROM TABLE1 WHERE ID > TBL1.ID AND ALLOCATION!=0))) IS NULL THEN SDATE
        ELSE (SELECT SDATE FROM TABLE1 WHERE ID =(SELECT MAX(ID) FROM TABLE1 WHERE ID >TBL1.ID AND ID<(SELECT MIN(ID) FROM TABLE1 WHERE ID > TBL1.ID AND ALLOCATION!=0))) END
        )ELSE (SELECT SDATE FROM TABLE1 WHERE ID = (SELECT MAX(ID) FROM TABLE1 WHERE ID > TBL1.ID ))END AS EDATE
        ,CASE WHEN (SELECT COUNT(*)-1 FROM TABLE1 WHERE ID BETWEEN TBL1.ID AND (SELECT MIN(ID) FROM TABLE1 WHERE ID > TBL1.ID AND ALLOCATION!=0)) <0 THEN 
        (SELECT COUNT(*) FROM TABLE1 WHERE ID BETWEEN TBL1.ID AND (SELECT MAX(ID) FROM TABLE1 WHERE ID > TBL1.ID )) ELSE
        (SELECT COUNT(*)-1 FROM TABLE1 WHERE ID BETWEEN TBL1.ID AND (SELECT MIN(ID) FROM TABLE1 WHERE ID > TBL1.ID AND ALLOCATION!=0)) END AS DAYCOUNT
        FROM TABLE1 TBL1 WHERE ALLOCATION = 0
        AND (((SELECT ALLOCATION FROM TABLE1 WHERE ID=(SELECT MAX(ID) FROM TABLE1  WHERE ID < TBL1.ID))<> 0 ) OR (SELECT MAX(ID) FROM TABLE1  WHERE ID < TBL1.ID)IS NULL); 
        

        【讨论】:

          【解决方案6】:

          没有 CTE 的解决方案:

          SELECT a.aDate AS StartDate
              , MIN(c.aDate) AS EndDate
              , (datediff(day, a.aDate, MIN(c.aDate)) + 1) AS DayCount
          FROM (
              SELECT x.aDate, x.allocation, COUNT(*) idn FROM table1 x
              JOIN table1 y ON y.aDate <= x.aDate
              GROUP BY x.id, x.aDate, x.allocation
          ) AS a
          LEFT JOIN (
              SELECT x.aDate, x.allocation, COUNT(*) idn FROM table1 x
              JOIN table1 y ON y.aDate <= x.aDate
              GROUP BY x.id, x.aDate, x.allocation
          ) AS b ON a.idn = b.idn + 1 AND b.allocation = a.allocation
          LEFT JOIN (
              SELECT x.aDate, x.allocation, COUNT(*) idn FROM table1 x
              JOIN table1 y ON y.aDate <= x.aDate
              GROUP BY x.id, x.aDate, x.allocation
          ) AS c ON a.idn <= c.idn AND c.allocation = a.allocation
          LEFT JOIN (
              SELECT x.aDate, x.allocation, COUNT(*) idn FROM table1 x
              JOIN table1 y ON y.aDate <= x.aDate
              GROUP BY x.id, x.aDate, x.allocation
          ) AS d ON c.idn = d.idn - 1 AND d.allocation = c.allocation
          WHERE b.idn IS NULL AND c.idn IS NOT NULL AND d.idn IS NULL AND a.allocation = 0
          GROUP BY a.aDate
          

          Example

          【讨论】:

          • 运行此程序时,我收到以下错误消息:Msg 530, Level 16, State 1, Line 1 语句终止。语句c之前最大递归100已经用完
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2013-06-11
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-02-05
          • 1970-01-01
          • 2012-04-22
          相关资源
          最近更新 更多