【问题标题】:SQL - Convert Time Series Events into On/Off Pairs (handling potential missing On's or Off's)SQL - 将时间序列事件转换为开/关对(处理可能丢失的开或关)
【发布时间】:2021-02-13 13:09:12
【问题描述】:

在 SQL Server 中,我有一组时间序列开/关事件,如下所示(为简单起见,我只显示了一个警报编号,但同一张表中有很多):

'Alarms' Table:
AlarmNumber   Time                      AlarmState
1592          2020-01-02 01:52:02       1
1592          2020-01-02 01:58:07       0
1592          2020-04-28 03:46:49       1
1592          2020-04-28 06:19:10       0
1592          2020-06-04 00:25:22       1
1592          2020-08-27 01:57:03       1
1592          2020-08-27 05:16:32       0
1592          2020-09-17 02:51:57       0

我正在尝试将其转换为开/关对:

Output I am trying to achieve, ideally as an SQL View:
AlarmNumber   StartTime                 EndTime
1592          2020-01-02 01:52:02       2020-01-02 01:58:07
1592          2020-04-28 03:46:49       2020-04-28 06:19:10
1592          2020-06-04 00:25:22       NULL
1592          2020-08-27 01:57:03       2020-08-27 05:16:32
1592          NULL                      2020-09-17 02:51:57

如果我有一个干净的数据集,没有丢失“开启”或“关闭”事件,我可以通过以下方式实现:

select tOn.AlarmNumber, tOn.Time StartTime, tOff.Time EndTime
from (
select AlarmNumber, Time, 
       ROW_NUMBER() Over(Partition by AlarmNumber order by Time) EventID
from Alarms where AlarmState = 1
) tOn
LEFT JOIN (
select AlarmNumber, Time, 
       ROW_NUMBER() Over(Partition by AlarmNumber order by Time) EventID
from Alarms where AlarmState = 0
) tOff
on (tOn.AlarmNumber = tOff.AlarmNumber and tOn.EventID = tOff.EventID)

(代码修改自 Adriano Carneiro 在T-SQL Start and end date times from a single column 的回答)

我的问题:谁能想到一种处理“警报”表的有效方法来实现我的示例输出,它处理丢失的开/关事件(在示例输出中显示为 NULL)?

我的备份是使用 Cursor 和 Where 循环,但我希望有一种方法可以将成对的 On/Off 事件组合在一起,但我无法让它工作。我有超过 500k 的事件,所以这是一个需要迭代的大型数据集。

欢迎任何想法!

谢谢你, 托马斯

------ 2020 年 11 月 1 日更新 ------

我们提供了两种出色的解决方案,它们都可以正常工作,并在 80,000 行杂乱的现实世界数据的样本中提供相同的结果。

  • GMB 的解决方案阅读起来更简单,但运行起来有点慢
  • gotqn 的解决方案是代码行更多,但在我的测试服务器上运行速度提高了 50% 左右

【问题讨论】:

    标签: sql sql-server tsql datetime gaps-and-islands


    【解决方案1】:

    一个组由两行连续组成,其中第一行的状态为 1,第二行的状态为 0。我会使用窗口函数来处理这个问题,如下所示:

    select 
        alarmnumber,
        max(case when alarmstate = 1 then time end) start_time,
        max(case when alarmstate = 0 then time end) end_time
    from (
        select a.*, 
            sum(case when alarmstate = 0 and lag_alarmstate = 1 then 0 else 1 end)
                over(partition by alarmnumber order by time) grp
        from (
            select a.*, 
                lag(alarmstate) over(partition by alarmnumber order by time) lag_alarmstate
            from alarms a
        ) a
    ) a
    group by alarmnumber, grp
    

    这使用lag() 检索“先前”状态,并使用累积和来定义组。最后一步是条件聚合。

    Demo on DB Fiddle

    报警号码 |开始时间 |时间结束 :------------ | :------------------------ | :------------------------ 第1592章2020-01-02 01:52:02.000 | 2020-01-02 01:58:07.000 第1592章2020-04-28 03:46:49.000 | 2020-04-28 06:19:10.000 第1592章2020-06-04 00:25:22.000 | 第1592章2020-08-27 01:57:03.000 | 2020-08-27 05:16:32.000 第1592章 | 2020-09-17 02:51:57.000

    【讨论】:

    • 感谢简单的解决方案!你的方法很容易遵循。我比较了两个答案的结果,现在它们在 80,000 个事件的数据集上提供了完全相同的输出。 gotqn 的解决方案更复杂,但运行时间为 600 毫秒,而此答案为 1200 毫秒。两者都很好用,谢谢。
    【解决方案2】:

    一旦有行顺序,只需SELECT 将它们分成几部分,然后使用UNION ALL 合并结果:

    DECLARE @DataSource TABLE
    (
        [AlarmNumber] INT
       ,[Time] DATETIME2(0)
       ,[AlarmState] INT
    );
    
    INSERT INTO @DataSource ([AlarmNumber], [Time], [AlarmState])
    VALUES (1592, '2020-01-02 01:52:02', 1)
          ,(1592, '2020-01-02 01:58:07', 0)
          ,(1592, '2020-04-28 03:46:49', 1)
          ,(1592, '2020-04-28 06:19:10', 0)
          ,(1592, '2020-06-04 00:25:22', 1)
          ,(1592, '2020-08-27 01:57:03', 1)
          ,(1592, '2020-08-27 05:16:32', 0)
          ,(1592, '2020-09-17 02:51:57', 0);
    
    -- Add a rowID column to the data
    WITH DataSource AS
    (
        SELECT * ,ROW_NUMBER() Over(Partition by AlarmNumber order by [Time]) rowID
        FROM @DataSource
    )
    
    -- This is just here so we can sort the result at the end
    SELECT * FROM (
    
    -- Select rows of DataSource where there is an ON and subsequent OFF event (DS1 Alarm is ON and DS2 Alarm is OFF)
    -- This also catches where there is an ON, but no subsequent OFF (DS2.Time will be NULL)
        SELECT DS1.AlarmNumber
                ,DS1.Time As StartTime
                ,DS2.Time As EndTime
        FROM DataSource DS1
        LEFT JOIN DataSource DS2
            ON DS1.[rowID] = DS2.[rowID] - 1
            AND DS1.AlarmNumber = DS2.AlarmNumber
            AND DS2.[AlarmState] = 0
        WHERE DS1.[AlarmState] = 1
    
        UNION ALL
    
        -- Select rows of DataSource where there is an OFF and there is no matching ON (aka it turned OFF without ever turning ON)
        SELECT DS2.AlarmNumber
                ,NULL As StartTime
                ,DS2.Time As EndTime
        FROM DataSource DS2
    
        INNER JOIN DataSource DS1
            ON DS2.[rowID] -1 = DS1.[rowID]
            AND DS1.[AlarmState] = 0
            AND DS2.AlarmNumber = DS1.AlarmNumber
        
        WHERE DS2.[AlarmState] = 0
    
        UNION ALL
    
        -- Select rows of DataSource where the first event for this alarm number is an OFF (it would otherwise be missed by the above)
        SELECT DS1.AlarmNumber
                ,NULL As StartTime
                ,DS1.Time As EndTime
        FROM DataSource DS1
        WHERE DS1.[AlarmState] = 0 AND DS1.rowID = 1
    ) z
    ORDER BY COALESCE(StartTime,EndTime), AlarmNumber
    

    【讨论】:

      猜你喜欢
      • 2021-03-02
      • 2021-03-05
      • 2017-06-07
      • 1970-01-01
      • 2015-10-03
      • 2022-01-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多