【问题标题】:Merge rows if date columns are overlapping in TSQL如果日期列在 SQL 中重叠,则合并行
【发布时间】:2019-05-07 08:31:39
【问题描述】:

我有一个格式如下的表格

Id  StartDate   EndDate Type
1   2012-02-18  2012-03-18  1
1   2012-03-17  2012-06-29  1
1   2012-06-27  2012-09-27  1
1   2014-08-23  2014-09-24  3
1   2014-09-23  2014-10-24  3
1   2014-10-23  2014-11-24  3
2   2015-07-04  2015-08-06  1
2   2015-08-04  2015-09-06  1
3   2013-11-01  2013-12-01  0
3   2018-01-09  2018-02-09  0

我在这里发现了类似的问题,但不能帮助我解决问题。我想合并具有相同IdType 和重叠日期期间的行。

上表的结果应该是

Id  StartDate   EndDate Type
1   2012-02-18  2012-09-27  1
1   2014-08-23  2014-11-24  3
2   2015-07-04  2015-09-06  1
3   2013-11-01  2013-12-01  0
3   2018-01-09  2018-02-09  0

在另一台服务器上,我可以通过以下限制和以下查询来做到这一点:

  • 不关心Type 列,只关心Id
  • 有较新版本的 SQL Server (2012),但现在我有 2008,但代码不兼容

SELECT Id
     , MIN(StartDate) AS StartDate
     , MAX(EndDate) AS EndDate
FROM (
    SELECT *
         , SUM(CASE WHEN a.EndDate = a.StartDate THEN 0
                    ELSE 1
               END
           ) OVER (ORDER BY Id, StartDate) sm
    FROM (
        SELECT Id
             , StartDate
             , EndDate
             , LAG(EndDate, 1, NULL) OVER (PARTITION BY Id ORDER BY Id, EndDate) EndDate
        FROM #temptable
    ) a
) b
GROUP BY Id, sm

任何建议,我该怎么做

  • 在进程中包含类型
  • 让它在 SQL Server 2008 上运行

【问题讨论】:

  • Id=3..使用什么逻辑?
  • 很抱歉。写问题时犯了一个错误。我修好了
  • 您所有的行都有 14 天的间隔。如果少于 14 天的间隔,那么 OP 的样子..?例如:1 2014-10-23 2014-10-24 3
  • 它只需要日期的最小值和最大值。正如 14 天是日期期间的实际部分一样。
  • 我认为这与我的问题相似。我有与您相同的列,但我继续使用ROW_NUMBER()PARTITION BY 从我的两个起始日期间隔列中为“连续范围”、“new_period”、“new_period_starting_id”等创建一些额外的帮助列。这是my approach and solution。不幸的是,我没有完整的代码了,但这是我的方法。

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


【解决方案1】:

此方法使用额外的临时表来识别重叠日期的组,然后根据分组执行快速聚合。

SELECT *, ROW_NUMBER() OVER (ORDER BY Id, Type) AS UID,
    ROW_NUMBER() OVER (ORDER BY Id, Type) AS GroupId INTO #G FROM #TempTable
WHILE @@ROWCOUNT <> 0 BEGIN
    UPDATE T1 SET
        GroupId = T2.GroupId
    FROM #G T1
        INNER JOIN (
            SELECT T1.UID, CASE WHEN T1.GroupId  < T2.GroupId THEN T1.GroupId ELSE T2.GroupId END
            FROM #G T1
                LEFT OUTER JOIN #G T2
                    ON T1.Id = T2.Id AND T1.Type = T2.Type AND T1.GroupId <> T2.GroupId
                        AND T1.StartDate <= T2.EndDate AND T2.StartDate <= T1.EndDate
            ) T2 (UID, GroupId)
            ON T1.UID = T2.UID
    WHERE T1.GroupId <> T2.GroupId
END
SELECT Id, MIN(StartDate) AS StartDate, MAX(EndDate) AS EndDate, Type
FROM #G G GROUP BY GroupId, Id, Type

这将返回预期值

Id          StartDate  EndDate    Type
----------- ---------- ---------- -----------
1           2012-02-18 2012-09-27 1
1           2014-08-23 2014-11-24 3
2           2015-07-04 2015-09-06 1
3           2013-11-01 2013-12-01 0
3           2018-01-09 2018-02-09 0

【讨论】:

  • 它工作正常。如果有性能更好的答案,我会保持悬赏开放,但如果没有其他人出现,我选择你的作为获得悬赏的最佳选择。非常感谢
  • 太好了,很高兴它对你有用。希望你找到最好的方法。对于 SQL 2008,我希望您会看到 2 个基本解决方案 - (1) 一些像这样的不定迭代分组方法或 (2) 递归 CTE,例如来自 Jordan。两者都是正确的,根据您的源数据和重叠日期的复杂性,任何一个都可能更快。
  • 对于 10 行,我发现 CTE 方法快了大约 2 倍。对于具有相同复杂程度的 100 万行(大约 2-5 个重叠),我发现迭代方法比 CTE 快约 3 倍。请注意,如果您使用 CTE,如果源数据中有超过 1000 个重叠,则可能必须设置 MAXRECURSION 选项。
  • @JasonW,这看起来像是Packing Intervals 问题。 Itzik Ben-Gan 提出了一个很好的解决方案。
  • 非常好 - 我也看到过类似的解决岛屿和差距 (red-gate.com/simple-talk/sql/t-sql-programming/…) 问题的方法。
【解决方案2】:

这是 2008 兼容的。在我看来,CTE 确实是连接所有重叠记录的最佳方式。日期重叠逻辑来自这个线程:SO Date Overlap

我添加了更复杂的额外数据,以确保它按预期工作。

DECLARE @Data table (Id INT, StartDate DATE, EndDate DATE, Type INT)
INSERT INTO @data 

SELECT 1,'2/18/2012' ,'3/18/2012', 1 UNION ALL
select 1,'3/17/2012','6/29/2012',1 UNION ALL
select 1,'6/27/2012','9/27/2012',1 UNION ALL
select 1,'8/23/2014','9/24/2014',3 UNION ALL
select 1,'9/23/2014','10/24/2014',3 UNION ALL
select 1,'10/23/2014','11/24/2014',3 UNION ALL
select 2,'7/4/2015','8/6/2015',1 UNION ALL
select 2,'8/4/2015','9/6/2015',1 UNION ALL
select 3,'11/1/2013','12/1/2013',0 UNION ALL
select 3,'1/9/2018','2/9/2018',0 UNION ALL 
select 4,'1/1/2018','1/2/2018',0 UNION ALL --many non overlapping dates
select 4,'1/4/2018','1/5/2018',0 UNION ALL
select 4,'1/7/2018','1/9/2018',0 UNION ALL
select 4,'1/11/2018','1/13/2018',0 UNION ALL

select 4,'2/7/2018','2/8/2018',0 UNION ALL --many overlapping dates
select 4,'2/8/2018','2/9/2018',0 UNION ALL 
select 4,'2/9/2018','2/10/2018',0 UNION all
select 4,'2/10/2018','2/11/2018',0 UNION all
select 4,'2/11/2018','2/12/2018',0 UNION all
select 4,'2/12/2018','2/13/2018',0 UNION all

select 4,'3/7/2018','3/8/2018',0 UNION ALL --many overlapping dates, second instance of id 4, type 0
select 4,'3/8/2018','3/9/2018',0 UNION ALL 
select 4,'3/9/2018','3/10/2018',0 UNION all
select 4,'3/10/2018','3/11/2018',0 UNION all
select 4,'3/11/2018','3/12/2018',0 UNION all
select 4,'3/12/2018','3/13/2018',0 



;
WITH cdata
AS (SELECT  Id,
        d.Type,
        d.StartDate,
        d.EndDate,
        CurrentStart = d.StartDate
    FROM    @Data d
    WHERE
        NOT EXISTS (
            SELECT * FROM @Data x WHERE x.StartDate < d.StartDate AND d.StartDate <= x.EndDate AND d.EndDate >= x.StartDate AND d.Id = x.Id AND d.Type = x.Type --get first records for overlapping ranges

                )       
    UNION ALL
    SELECT  d.Id,
        d.Type,
        StartDate = CASE WHEN d2.StartDate < d.StartDate THEN d2.StartDate ELSE d.StartDate END,
        EndDate = CASE WHEN d2.EndDate > d.EndDate THEN d2.EndDate ELSE d.EndDate END,
        CurrentStart = d2.StartDate
    FROM    cdata d
        INNER JOIN @Data d2
            ON (
                d.StartDate <= d2.EndDate
                AND d.EndDate >= d2.StartDate
            ) 
            AND d2.Id = d.Id
            AND d2.Type = d.Type
            AND d2.StartDate > d.CurrentStart)
SELECT cdata.Id, cdata.Type, cdata.StartDate, EndDate = MAX(cdata.EndDate) 
FROM        cdata 
GROUP BY cdata.Id, cdata.Type, cdata.StartDate

【讨论】:

    【解决方案3】:

    这看起来像是Packing Intervals 问题。请参阅 Itzik Ben-Gan 的帖子以了解所有详细信息以及他建议使用哪些索引以使其高效运行。他提出了一个没有递归 CTE 的解决方案。

    两个音符。

    1. 下面的查询假设区间是 [close;打开),即StartDate 是包容性的,EndDate 是独占性的。这种表示此类数据的方式通常是最方便的。 (就像在编程语言中使用从 0 开始而不是从 1 开始的数组通常更方便一样)。

    2. 我添加了一个RowID 列以进行明确的排序。

    样本数据

    DECLARE @T TABLE
    (
        RowID int IDENTITY,
        id int,
        StartDate date,
        EndDate date,
        tp int
    );
    
    INSERT INTO @T(Id, StartDate, EndDate, tp) VALUES
    (1,  '2012-02-18',  '2012-03-18',  1),
    (1,  '2012-03-17',  '2012-06-29',  1),
    (1,  '2012-06-27',  '2012-09-27',  1),
    (1,  '2014-08-23',  '2014-09-24',  3),
    (1,  '2014-09-23',  '2014-10-24',  3),
    (1,  '2014-10-23',  '2014-11-24',  3),
    (2,  '2015-07-04',  '2015-08-06',  1),
    (2,  '2015-08-04',  '2015-09-06',  1),
    (3,  '2013-11-01',  '2013-12-01',  0),
    (3,  '2018-01-09',  '2018-02-09',  0);
    
    -- Make EndDate an opened interval, make it exclusive
    -- [Start; End)
    UPDATE @T
    SET EndDate = DATEADD(day, 1, EndDate)
    ;
    

    推荐索引

    -- indexes to support solutions
    CREATE UNIQUE INDEX idx_start_id ON T(id, tp, StartDate, RowID);
    CREATE UNIQUE INDEX idx_end_id ON T(id, tp, EndDate, RowID);
    

    查询

    阅读 Itzik 的帖子以了解发生了什么。他那里有很好的插图。简而言之,每个时间戳(开始或结束)都被视为一个事件。每个事件都有一个+- 类型。每次遇到+ 事件(某个间隔开始)时,我们都会增加运行计数器。每次遇到- 事件(某个间隔结束)时,我们都会减少运行计数器。当运行计数器为 0 时,表示重叠间隔的连续结束。

    我按原样接受了 Itzik 的查询,只是更改了列名以匹配您的姓名。

    WITH C1 AS
    -- let e = end ordinals, let s = start ordinals
    (
        SELECT
            RowID, id, tp, StartDate AS ts, +1 AS EventType,
            NULL AS e,
            ROW_NUMBER() OVER(PARTITION BY id, tp ORDER BY StartDate, RowID) AS s
        FROM @T
    
        UNION ALL
    
        SELECT
            RowID, id, tp, EndDate AS ts, -1 AS EventType,
            ROW_NUMBER() OVER(PARTITION BY id, tp ORDER BY EndDate, RowID) AS e,
            NULL AS s
        FROM @T
    ),
    C2 AS
    -- let se = start or end ordinal, namely, how many events (start or end) happened so far
    (
        SELECT C1.*,
        ROW_NUMBER() OVER(PARTITION BY id, tp ORDER BY ts, EventType DESC, RowID) AS se
        FROM C1
    ),
    C3 AS
    -- For start events, the expression s - (se - s) - 1 represents how many sessions were active
    -- just before the current (hence - 1)
    --
    -- For end events, the expression (se - e) - e represents how many sessions are active
    -- right after this one
    --
    -- The above two expressions are 0 exactly when a group of packed intervals
    -- either starts or ends, respectively
    --
    -- After filtering only events when a group of packed intervals either starts or ends,
    -- group each pair of adjacent start/end events
    (
        SELECT id, tp, ts,
            ((ROW_NUMBER() OVER(PARTITION BY id, tp ORDER BY ts) - 1) / 2 + 1)
            AS grpnum
        FROM C2
        WHERE COALESCE(s - (se - s) - 1, (se - e) - e) = 0
    )
    SELECT id, tp, MIN(ts) AS StartDate, DATEADD(day, -1, MAX(ts)) AS EndDate
    FROM C3
    GROUP BY id, tp, grpnum
    ORDER BY id, tp, StartDate;
    

    结果

    +----+----+------------+------------+
    | id | tp | StartDate  |  EndDate   |
    +----+----+------------+------------+
    |  1 |  1 | 2012-02-18 | 2012-09-27 |
    |  1 |  3 | 2014-08-23 | 2014-11-24 |
    |  2 |  1 | 2015-07-04 | 2015-09-06 |
    |  3 |  0 | 2013-11-01 | 2013-12-01 |
    |  3 |  0 | 2018-01-09 | 2018-02-09 |
    +----+----+------------+------------+
    

    【讨论】:

      【解决方案4】:
      create table #table
      (Id int,StartDate date,  EndDate date, Type int)
      
      insert into #table
      values
      
      ('1','2012-02-18','2012-03-18','1'),('1','2012-03-19','2012-06-19','1'),
      ('1','2012-06-27','2012-09-27','1'),('1','2014-08-23','2014-09-24','3'),
      ('1','2014-09-23','2014-10-24','3'),('1','2014-10-23','2014-11-24','3'),
      ('2','2015-07-04','2015-08-06','1'),('2','2015-08-04','2015-09-06','1'),
      ('3','2013-11-01','2013-12-01','0'),('3','2018-01-09','2018-02-09','0')
      
      select ID,MIN(startdate)sd,MAX(EndDate)ed,type from #table
      group by ID,TYPE,YEAR(startdate),YEAR(EndDate)
      

      【讨论】:

      • 不幸的是,这不起作用。它合并日期而不检查它们之间的差距
      【解决方案5】:

      这可以通过使用一些窗口函数和 CTE 轻松实现。这是解决方案

      DECLARE @table TABLE
      (id        INT, 
       StartDate DATE, 
       EndDate   DATE, 
       [Type]    INT
      );
      
      INSERT INTO @table(Id,  StartDate,  EndDate,  [Type]) VALUES
      (1,  '2012-02-18',  '2012-03-18',  1),
      (1,  '2012-03-17',  '2012-06-29',  1),
      (1,  '2012-06-27',  '2012-09-27',  1),
      (1,  '2014-08-23',  '2014-09-24',  3),
      (1,  '2014-09-23',  '2014-10-24',  3),
      (1,  '2014-10-23',  '2014-11-24',  3),
      (2,  '2015-07-04',  '2015-08-06',  1),
      (2,  '2015-08-04',  '2015-09-06',  1),
      (3,  '2013-11-01',  '2013-12-01',  0),
      (3,  '2018-01-09',  '2018-02-09',  0);
      
      WITH C1 AS
      (
        SELECT *,
          MAX(EndDate) OVER(PARTITION BY Id, [Type]
                ORDER BY StartDate, EndDate
                ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING) AS PrevEnd
        FROM @table
      ),
      C2 AS
      (
        SELECT *,
          SUM(StartFlag) OVER(PARTITION BY Id, [Type]
                ORDER BY StartDate, EndDate
                ROWS UNBOUNDED PRECEDING) AS GroupID
        FROM C1
          CROSS APPLY ( VALUES(CASE WHEN StartDate <= PrevEnd THEN NULL ELSE 1 END) ) AS A(StartFlag)
      )
      SELECT Id, [Type], MIN(StartDate) AS StartDate, MAX(EndDate) AS EndDate
      FROM C2
      GROUP BY Id, [Type], GroupID;
      

      【讨论】:

      • 我在这里收到此错误Incorrect syntax near 'ROWS'。您确定它与 SQL Server 2008 兼容吗?
      猜你喜欢
      • 1970-01-01
      • 2021-04-01
      • 1970-01-01
      • 1970-01-01
      • 2021-10-11
      • 1970-01-01
      • 1970-01-01
      • 2019-07-09
      • 1970-01-01
      相关资源
      最近更新 更多