【问题标题】:SQL - Group records in together based on start and end eventsSQL - 根据开始和结束事件将记录分组在一起
【发布时间】:2020-03-09 18:23:32
【问题描述】:

我有一张桌子,基本上就是我下面的桌子。对于每个 Event1 记录,有多个 Event2 记录,每个记录都有一个状态。

  Event1   Event2   Status  
 -------- -------- -------- 
       1        1   Start   
       1        2   Middle  
       1        3   Middle  
       1        4   End     
       1        5   Start   
       1        6   Middle  
       1        7   Middle  
       2       10   Start   
       2       11   Middle  
       2       12   End     
       2       13   Start   
       2       14   End     
       2       15   Start  

我希望能够对数据进行分组,以同时显示所有独特的开始状态和匹配的结束状态。所有 ID 都是按时间顺序编写的,因此每个 Event2 分组都将以 Start 状态开始,然后可能有也可能没有中间状态,然后可能以 End 状态结束。每个 Event1 记录不应有两个或多个 Start 状态,它们之间没有 End 状态。

  Event1   StartEvent2   EndEvent2  
 -------- ------------ ----------- 
       1            1           4         
       1            5        NULL      
       2           10          12        
       2           13          14        
       2           15        NULL   

你能帮我用 SQL 查询来生成这个结果吗?谢谢。

编辑:我忘了提到在“结束”事件之前可能不止“开始”事件。但我需要忽略中间的“开始”事件,只保留第一个“开始”。所以它可以开始>开始>中间>结束,但我只想保留第一个开始和结束。

【问题讨论】:

  • 是的,我有。但我还没有完全正确,只是因为数据的时髦。必须用完,所以我稍后再回复。

标签: sql sql-server database window-functions gaps-and-islands


【解决方案1】:

欢迎来到 StackOverflow,

GMB 非常成功,这是一个空白和孤岛问题。

我相信这应该会为您提供所需的结果。如果您专注于特定的Event1 值,则获取Event1 的所有数据并将其放入#temp 表中然后针对该#temp 表执行此操作可能会更快。

从名为X 的CTE 开始——我们计算一个名为GroupId 的列,该列是由Event1 分区的Row_Number(),减去由Event1,Status 分区的Row_Number()。然后我们在 CTE Y 中使用 CTE X,并在 Event1,Status,GroupId 上进行另一个 Row_Number() 分区以生成下表:

Event1  Event2  Status  GroupId GID
1       1       Start   0       1
1       4       End     1       1
1       5       Start   1       1
2       10      Start   0       1
2       12      End     1       1
2       13      Start   1       1
2       14      End     2       1
2       15      Start   2       1
3       16      Start   0       1
3       17      Start   0       2
3       18      End     2       1

这使我们可以灵活地为Event1=3Event2=17 放弃额外的“开始”。

然后,我们将 CTE Y (Where Gid=1) 传递给名为 Z 的最终 CTE。在此 CTE 中,我们在 Event1 上进行了最终的 Row_Number() 分区,并使用 1/0 表示开始/结束组合。

DECLARE @data TABLE ([Event1] INT, [Event2] INT, [Status] VARCHAR(100))
INSERT INTO @data ([Event1],[Event2],[Status])
VALUES (1,1,'Start'),
        (1,2,'Middle'),
        (1,3,'Middle'),
        (1,4,'End'),
        (1,5,'Start'),
        (1,6,'Middle'),
        (1,7,'Middle'),
        (2,10,'Start'),
        (2,11,'Middle'),
        (2,12,'End'),
        (2,13,'Start'),
        (2,14,'End'),
        (2,15,'Start'),
        (3,16,'Start'),
        (3,17,'Start'),
        (3,18,'End')

;WITH X AS
(
    SELECT *,
            ROW_NUMBER() OVER(PARTITION BY [Event1] ORDER BY [Event2]) - ROW_NUMBER() OVER(PARTITION BY [Event1],[Status] ORDER BY [Event2]) AS [GroupId]
    FROM @data
    WHERE [Status] IN ('Start','End')

), Y AS
    (
        SELECT *, ROW_NUMBER() OVER(PARTITION BY [Event1],[Status],[GroupId] ORDER BY [Event2]) AS [GID]
        FROM X
    ), Z AS
            (
                SELECT *,
                    ROW_NUMBER() OVER(PARTITION BY [Event1], CASE WHEN [Status]='Start' THEN 1 ELSE 0 END ORDER BY [Event2]) AS [RN]
                FROM Y
                WHERE [GID]=1
            )

SELECT T1.[Event1], T1.Event2 AS [StartEvent2], T2.Event2 AS [EndEvent2]
FROM Z T1
LEFT JOIN Z T2 ON T1.[Event1]=T2.[Event1] AND T1.[RN]=T2.[RN] AND T2.[Status]='End'
WHERE T1.[Status]='Start'
ORDER BY T1.[Event1], T1.[Event2]

最终结果为:

Event1  StartEvent2 EndEvent2
1       1           4
1       5           NULL
2       10          12
2       13          14
2       15          NULL
3       16          18

【讨论】:

    【解决方案2】:

    这是某种差距和孤岛问题。我会使用定义组的累积窗口总和来解决这个问题,row_number() 来识别组中的最后一条记录,然后聚合:

    select 
        Event1,
        min(Event2) StartEvent2,
        max(case when rn = 1 and status = 'End' then Event2 end) EndEvent2
    from (
        select 
            t.*,
            row_number() over(partition by Event1, grp order by Event2 desc) rn
        from (
            select
                t.*,
                sum(case when Status = 'Start' then 1 else 0 end) 
                    over(partition by Event1 order by Event2) grp
            from mytable t
        ) t
    ) t
    group by Event1, grp
    order by Event1, grp
    

    这正确地解决了End 出现在组中间的情况(如开始 -> 中间 -> 结束 -> 中间 -> 开始...),在这种情况下,我会假设你不想'不想把它当作一个结束事件。

    Demo on DB Fiddle详细说明了流程的每个步骤。最终查询返回:

    事件1 |开始事件2 |结束事件2 -----: | ----------: | --------: 1 | 1 | 4 1 | 5 | 2 | 10 | 12 2 | 13 | 14 2 | 15 |

    【讨论】:

    • 我实际上使用的是 SQL 2008,所以 LAST_VALUE 似乎不是一个有效的函数。不过还是谢谢。
    • @aleslie:好的。我用使用row_number() 而不是last_value() 的解决方案更新了我的答案。
    【解决方案3】:

    您无需将事件组合在一起。为此,我建议使用相关子查询或 apply

    select r.*,
           (select min(r2.event2)
            from records r2
            where r2.event1 = r.event1 and r2.status = 'End' and
                  r2.event2 > r.event2
           ) as end_event
    from records r
    where r.status = 'Start';
    

    使用(event1, event2, status) 上的索引,性能应该没问题。但您也可以考虑使用 windows 函数方法:

    select r.*
    from (select r.*,
                 min(case when status = 'End' then event2 end) over (partition by event1 order by event2 desc) as end_event
          from records r
         ) r
    where r.status = 'Start';
    

    【讨论】:

    • 谢谢。这工作,几乎。但是我忘记了数据中的一件关键事情是我的错。我忘了提到在“结束”事件之前可能不止有“开始”事件。但我需要忽略中间的“开始”事件,只保留第一个“开始”。所以它可以开始>开始>中间>结束,但我只想保留第一个开始和结束。
    • @aleslie 。 . .我建议您撤消对此问题的编辑并提出一个新问题。
    【解决方案4】:

    这实际上非常简单,只需一点条件聚合和一个窗口函数:

    WITH Grps AS(
        SELECT V.Event1,
               V.Event2,
               V.[status],
               COUNT(CASE V.[Status] WHEN 'Start' THEN 1 END) OVER (PARTITION BY V.Event1 ORDER BY V.Event2) AS Grp
        FROM (VALUES(1,1,'Start'),
                    (1,2,'Middle'),
                    (1,3,'Middle'),
                    (1,4,'End'),
                    (1,5,'Start'),
                    (1,6,'Middle'),
                    (1,7,'Middle'),
                    (2,10,'Start'),
                    (2,11,'Middle'),
                    (2,12,'End'),
                    (2,13,'Start'),
                    (2,14,'End'),
                    (2,15,'Start'))V(Event1,Event2,[Status]))
    SELECT Event1,
           MAX(CASE [Status] WHEN 'Start' THEN Event2 END) AS StartEvent2,
           MAX(CASE [Status] WHEN 'End' THEN Event2 END) AS StartEvent2
    FROM Grps
    GROUP BY Event1,
             Grp;
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-11-30
      • 1970-01-01
      • 2018-12-17
      • 1970-01-01
      相关资源
      最近更新 更多