SQL - 根据开始和结束事件将记录分组在一起

SQL - Group records in together based on start and end events

我有一个 table,基本上就是下面的内容。对于每个 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 分组都将以开始状态开始,然后可能有也可能没有中间状态,然后可能以结束状态结束。每个 Event1 记录不应有两个或多个开始状态,并且它们之间没有结束状态。

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

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

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

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

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;

这是某种间隙和孤岛问题。我会使用定义组的累积 window 总和来解决这个问题,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 详细说明了流程的每个步骤。最终查询 returns:

Event1 | StartEvent2 | EndEvent2
-----: | ----------: | --------:
     1 |           1 |         4
     1 |           5 |      null
     2 |          10 |        12
     2 |          13 |        14
     2 |          15 |      null

您不需要将事件组合在一起。为此,我建议使用相关子查询或 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';

欢迎来到 Whosebug,

专线小巴很顺利,这是一个缺口和孤岛问题。

我相信这应该能让您获得所需的结果。如果您专注于特定的 Event1 值,获取 Event1 的所有数据并将其放入 #temp table 然后针对该 #temp table.

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

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.

抛出额外的 "Start"

然后我们将 CTE Y (Where Gid=1) 传递到我们称为 Z 的最终 CTE 中。在此 CTE 中,我们在 Event1 上进行最终 Row_Number() 分区,并使用 1/0 表示 start/end 组合。

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