根据时间、ID 和事件类型从未排序的事件表中删除某些事件

Posted

技术标签:

【中文标题】根据时间、ID 和事件类型从未排序的事件表中删除某些事件【英文标题】:Removing certain events from an unsorted table of events based on time, id and type of event 【发布时间】:2018-03-18 06:22:06 【问题描述】:

TL;DR:我正在尝试从传入事件列表中删除特定类型事件的重复。我有 5-20 百万条记录。应该删除缩进的行:

sn#     time        mo  method
02848   1504725241  R   P
02848   1504725365  R   F1.0
    02848   1504725366  R   F1.6
    02848   1504725366  R   F2.0
02848   1504727651  R   P
02848   1504727681  R   P
02848   1504727741  R   F1.0
02848   1504728165  R   E

详细说明:

为了提供一些上下文,这些是执行的用户操作。其中模式是用户正在使用的服务类型。方法就是用户所做的。例如,P 是暂停的缩写,F 是快进的缩写。

我正在处理的表具有以下结构:

CREATE TABLE events
([serialnumber] int, [time] int, [mode] varchar(1), [method] varchar(4))

它每小时更新一次,包含 5 到 2000 万条记录。 使用如上所示的示例数据。

我希望创建会话以填写下表:

CREATE TABLE sessions(serialnumber int, mode varchar(1), method varchar(1), startTime int, endTime int);

我想要实现的是基于这些事件创建会话,并带有一个重要的过滤器。我不希望使用方法 F 重复事件,我只想要第一个 F 事件,连接到接下来发生的任何非 F 事件。也就是说,对于上述情况,我希望在处理后进行以下会话,即我想要的结果是:

#sn   metho mod starttime   endtime
02848   R   P   1504725241  1504725365
02848   R   F   1504725365  1504727651
02848   R   P   1504727651  1504727681      
02848   R   P   1504727681  1504727741  
02848   R   F   1504727741  1504728165

这里将F1.0、F1.6、F2.0等改为F,保留一组F事件的第一个时间戳,去掉了重复。由于大量记录,我无法有效地执行此操作。

截至目前,我正在尝试以下方法:

    更新表时,删除所有 F 事件,每组 F 事件中的第一个除外,pr 序列号。 使用cross apply 很像LAG 函数来生成会话列表。

完整的 SQL 代码如下所示:

--Delete recurring F events
DELETE outside
FROM events AS outside
CROSS APPLY
(
  SELECT TOP 1 inside.[Time]
  FROM events inside
  WHERE outside.serialnumber = inside.serialnumber
    AND (outside.[Time] > inside.[Time] 
     OR (outside.[Time] = inside.[Time] 
      AND outside.[method] != inside.[method]
      )
    )
    AND LEFT(outside.[method],1)  = 'F' 
    AND LEFT(inside.[method],1) = 'F'
    AND NOT EXISTS 
    (SELECT innerInside.[Time] FROM events innerInside
      WHERE innerInside.serialnumber = inside.serialnumber
        AND innerInside.[Time] <= outside.[Time]
        AND innerInside.[Time] > inside.[Time]
        AND LEFT(innerInside.[method],1)  != 'F'
  )
  ORDER BY inside.[Time] DESC
) processed_inside


--Create sessions from events
SELECT serialnumber
      ,outside.[mode]
      ,LEFT(outside.[method],1) AS 'Method'
      ,outside.[Time] AS 'SessionStart'
      ,processed_inside.[Time] AS 'SessionEnd'

FROM events AS outside
CROSS APPLY
     (
      SELECT MIN( inside.[Time]) AS 'Time'
      FROM   events AS inside
      WHERE  outside.serialnumber = inside.serialnumber
         AND outside.[Time] < inside.[Time]
      ) processed_inside
WHERE processed_inside.[Time] IS NOT NULL

这可行,但删除步骤慢得难以忍受。这是删除步骤的实际执行计划的图片。 (请注意,这是实际的数据库,因此它看起来与本文中的简化示例略有不同。)

我也考虑过将第 1 步作为第 2 步的一部分,但没有找到任何有意义的方法。最后,我正在寻找一种在不生成太多会话的情况下有效地创建会话的方法(因此删除 F 类型的事件)。

如果您一路走来,可以在这里找到 sqlfiddle:http://sqlfiddle.com/#!6/a5392/1。我在其中添加了上述两个功能。输出表就是想要的结果。

如果您能给我任何帮助,我将不胜感激。

【问题讨论】:

没有索引吗? 致对问题进行编辑的人:请阅读文本:他想为“缩进行”提供证据 @SqlZim 不,但我可以创建它们,只要 BULK INSERT 完成创建表,不会比我需要执行的操作慢。 @ewolden :您需要删除事件还是只是用来解决问题?我的意思是,如果可以根据要求生成“会话”而不删除事件,这对您有好处吗? @etsa 如果您可以生成会话而不删除,那将是完美的。 【参考方案1】:

第二次更新 20171009 15:35

这应该可以处理您的所有示例数据。 关于上一版本插件的一些解释:

Q1 查询中的最后一个条件(OR (RN=1 AND method='F') 是离开最后一个 F(因为后者对于计算聚合 F 的结束时间很有用);

在 Q11 中,我使用了两个 ROW_NUMBER() 来查找连续的方法和 在 WHERE 条件中,除了 最后一个(如果存在)

在 WITH 内的最后一个查询 (Q2) 中,结束时间是使用 (SELECT TOP 1 ....) 最后一个 WHERE 条件是删除(如果存在)结尾(但 已经累积)F 方法(见上面的第 1 点) 也请参阅我在以前版本中所说的内容。
WITH Q1 AS (SELECT * 
                        FROM (SELECT serialnumber, [time] AS TTIME, mode, LEFT(method,1) AS method, SUBSTRING(method,2,3) AS rest_of_method
                                , ROW_NUMBER() OVER (PARTITION BY serialnumber ORDER BY [time] DESC) AS RN
                              FROM events
                            )A
                              WHERE method<>'F' OR rest_of_method='1.0' OR (RN=1 AND method='F') 
                              /* AND serialnumber=6363*/  /* test*/             
                        )
            , Q11 AS (SELECT Q1a.serialnumber, Q1a.mode, Q1a.method, Q1a.starttime, RN
                      FROM (SELECT serialnumber, mode, method, TTIME AS starttime
                            , ROW_NUMBER() OVER (PARTITION BY serialnumber ORDER BY TTIME, method) AS consec_id1    
                            , RN            
                            FROM Q1) Q1a 
                            LEFT JOIN   (SELECT serialnumber, mode, method, TTIME AS starttime
                                        , ROW_NUMBER() OVER (PARTITION BY serialnumber ORDER BY TTIME, method) AS consec_id2
                                        FROM Q1) Q1b on Q1b.serialnumber=Q1a.serialnumber AND Q1b.consec_id2 = Q1a.consec_id1 - 1
                      WHERE (Q1b.consec_id2 IS NULL
                            OR Q1a.method<>'F'
                            OR Q1a.method <> Q1b.method                 
                            OR RN=1) 
                    )
            , Q2 AS (SELECT Q11.serialnumber
                    , Q11.mode
                    , Q11.method
                    , Q11.starttime
                    , (SELECT TOP 1 starttime FROM Q11 Q2 WHERE Q2.starttime>Q11.starttime AND Q2.SERIALNUMBER=Q11.SERIALNUMBER ORDER BY Q2.starttime) AS endtime
                    , RN
                    FROM Q11
                     )
            SELECT Q2.serialnumber, Q2.mode, Q2.method, Q2.starttime, Q2.endtime
            FROM Q2
            WHERE NOT (method='F' AND endtime IS NULL)
            ORDER BY Q2.serialnumber, Q2.starttime;

输出:

serialnumber mode method starttime   endtime
------------ ---- ------ ----------- -----------
2848         R    P      1504725241  1504725365
2848         R    F      1504725365  1504727651
2848         R    P      1504727651  1504727741
2848         R    F      1504727741  1504728165
2848         R    E      1504728165  1504728167
2848         L    B      1504728167  1504728171
2848         T    B      1504728171  1504728193
2848         T    P      1504728171  1504728193
2848         T    F      1504728193  1504728208
2848         L    B      1504728208  1504728215
2848         T    E      1504728208  1504728215
2848         T    F      1504728208  1504728215
2848         L    B      1504728215  1504728221
2848         L    B      1504728221  1504728225
2848         L    B      1504728225  1504728230
2848         L    B      1504728230  1504728234
2848         L    B      1504728234  1504728238
2848         L    B      1504728238  1504728249
2848         L    B      1504728249  1504728255
2848         L    B      1504728255  1504728300
2848         L    B      1504728300  1504728845
2848         S    S      1504728845  1504728846
2848         S    S      1504728846  NULL
6363         R    B      1504726264  1504726265
6363         R    F      1504726265  1504729288
6363         R    E      1504729288  1504729289
6363         L    B      1504729289  1504729318
6363         R    B      1504729318  1504731181
6363         R    F      1504729318  1504731181
53344        L    B      1504725984  1504725987
53344        L    B      1504725987  1504725988
53344        L    B      1504725988  1504725992
53344        L    B      1504725992  1504725993
53344        L    B      1504725993  NULL
78901        L    B      1504725485  1504725488
78901        L    B      1504725488  1504725491
78901        L    B      1504725491  NULL

更新 20171009 -- 目前仅适用于 2848

我修改了之前的查询(见底部)以解决连续 F1.0、Fx.xx、..、F1.0 的必要性(id est,您可以以 F1.0 开始和结束 Fs 序列)。 我认为,如果有一天您将迁移到 MSSQL2008+,可能会找到更好的解决方案。以下应该适用于 MSSQL2005。

关于性能: 我将 PK 添加到您的数据集(EVENTS_PK PRIMARY KEY(序列号,模式,方法,[时间]),我添加了一个索引(INDEX EVENTS_IXN01 ON EVENTS(序列号,时间))。

实际上,对于您的示例数据,我的查询仅使用索引 EVENTS_IXN01(但如果您不创建 PK,我认为您应该在其中包含一些列)。 我认为可以进行其他一些改进(例如,如果您将方法的第一个字母与其他字母分开)。 如果您决定只查询某些时间窗口(在这种情况下您需要更改/创建索引),则可以获得其他改进

WITH Q1 AS (SELECT * 
            FROM (SELECT serialnumber, [time] AS TTIME, mode, LEFT(method,1) AS method      
                  FROM events
                  WHERE (LEFT(method,1)<>'F' OR SUBSTRING(method,2,3)='1.0')
                  AND serialnumber=02848 /* test*/
                )A
            )
, Q2 AS (SELECT Q1.serialnumber
        , Q1.mode
        , Q1.method
        , Q1.TTIME AS starttime
        , (SELECT TOP 1 TTIME FROM Q1 Q2 WHERE Q2.TTIME>Q1.TTIME AND Q2.SERIALNUMBER=Q1.SERIALNUMBER ORDER BY Q2.TTIME) AS endtime
    FROM Q1
            )
SELECT Q4.serialnumber
        ,Q4.mode
        ,Q4.method
        ,Q4.starttime 
        ,Q4.endtime     
FROM (
    SELECT Q2.serialnumber
        ,Q2.mode
        ,Q2.method
        ,Q2.starttime 
        ,Q2.endtime+COALESCE(Q3.endtime-Q3.starttime,0) AS endtime
        ,Q3.starttime AS dupF_starttime
    FROM Q2
    LEFT JOIN Q2 Q3 ON Q2.serialnumber=Q3.serialnumber AND Q2.mode=Q3.mode AND Q2.method=Q3.method AND Q2.endtime=Q3.starttime AND Q2.method='F'
    ) Q4
WHERE dupF_starttime IS NULL
ORDER BY serialnumber, starttime;

输出

serialnumber mode method starttime   endtime
------------ ---- ------ ----------- -----------
2848         R    P      1504725241  1504725365
2848         R    F      1504725365  1504727651
2848         R    P      1504727651  1504727741
2848         R    F      1504727741  1504728165
2848         R    E      1504728165  1504728167
2848         L    B      1504728167  1504728171
2848         T    B      1504728171  1504728193
2848         T    P      1504728171  1504728193
2848         L    B      1504728208  1504728215
2848         T    E      1504728208  1504728215
2848         T    F      1504728208  1504728215
2848         L    B      1504728215  1504728221
2848         L    B      1504728221  1504728225
2848         L    B      1504728225  1504728230
2848         L    B      1504728230  1504728234
2848         L    B      1504728234  1504728238
2848         L    B      1504728238  1504728249
2848         L    B      1504728249  1504728255
2848         L    B      1504728255  1504728300
2848         L    B      1504728300  1504728845
2848         S    S      1504728845  1504728846
2848         S    S      1504728846  NULL
6363         R    B      1504726264  1504726265
6363         R    F      1504728525  1504729288
6363         R    E      1504729288  1504729289
6363         L    B      1504729289  1504729318
6363         R    B      1504729318  1504729334
6363         R    F      1504730579  NULL
53344        L    B      1504725984  1504725987
53344        L    B      1504725987  1504725988
53344        L    B      1504725988  1504725992
53344        L    B      1504725992  1504725993
53344        L    B      1504725993  NULL
78901        L    B      1504725485  1504725488
78901        L    B      1504725488  1504725491
78901        L    B      1504725491  NULL

上一个

这不是一个确定的答案。这是我发现发布它的唯一方法。

它不尊重您在评论中观察到的观点(“虽然假设第一个总是'F1.0',但你不能假设最后一个永远不是'F1.0'”)。

此刻(我现在必须离开)我假设在 Fs 序列中 F1.0 不能重复(我知道,这不是你要问的)。 所以我只使用了序列号 02848(我从 SQLFiddle 上的示例数据中删除了 (02848, 1504728208, 'T', 'F1.0') 行)。

也许星期一我可以继续处理查询,或者它可以为您提供一些建议。

WITH Q1 AS (SELECT * 
            FROM (SELECT serialnumber, [time] AS TTIME, mode, LEFT(method,1) AS method      
                  FROM events
                  WHERE 1=1
                  AND (LEFT(method,1)<>'F' OR SUBSTRING(method,2,3)='1.0')
                  AND serialnumber=02848 /* test*/
                )A
            )
SELECT Q1.serialnumber
    , Q1.mode
    , Q1.method
    , Q1.TTIME AS starttime
    , (SELECT TOP 1 TTIME FROM Q1 Q2 WHERE Q2.TTIME>Q1.TTIME AND Q2.SERIALNUMBER=Q1.SERIALNUMBER ORDER BY Q2.TTIME) AS endtime
FROM Q1
ORDER BY Q1.serialnumber, Q1.TTIME
;

我看到使用(但我无法进行许多其他测试)的性能改进:

CREATE INDEX EVENTS_IXN01 ON EVENTS (SERIALNUMBER, TIME);

输出:

serialnumber mode method starttime   endtime
------------ ---- ------ ----------- -----------
2848         R    P      1504725241  1504725365
2848         R    F      1504725365  1504727651
2848         R    P      1504727651  1504727741
2848         R    F      1504727741  1504728165
2848         R    E      1504728165  1504728167
2848         L    B      1504728167  1504728171
2848         T    B      1504728171  1504728193
2848         T    P      1504728171  1504728193
2848         T    F      1504728193  1504728208
2848         T    E      1504728208  1504728215
2848         L    B      1504728208  1504728215
2848         L    B      1504728215  1504728221
2848         L    B      1504728221  1504728225
2848         L    B      1504728225  1504728230
2848         L    B      1504728230  1504728234
2848         L    B      1504728234  1504728238
2848         L    B      1504728238  1504728249
2848         L    B      1504728249  1504728255
2848         L    B      1504728255  1504728300
2848         L    B      1504728300  1504728845
2848         S    S      1504728845  1504728846
2848         S    S      1504728846  NULL

【讨论】:

这看起来很有希望,并激发了我的一些想法。我同样要离开,直到星期一。将报告它的执行情况以及我是否找到一种方法来消除结果中出现的后续“F1.0”。谢谢! @ewolden -- 添加了另一个查询版本,它“删除”了后续 F1.0。添加了一些关于表演的注释。 @ewolden --- 添加了另一个(第 3 版)版本:它应该按要求工作(性能比以前差一点......) 非常感谢您花费的所有时间和精力。对此,我真的非常感激。我已经使用了您的最后一次迭代,并且看到效率有了巨大的提高。再次感谢您! 不客气。请记住,如果有一天你们都迁移到 SQLServer2008+,查询可能会被简化。

以上是关于根据时间、ID 和事件类型从未排序的事件表中删除某些事件的主要内容,如果未能解决你的问题,请参考以下文章

根据事件时间戳组合行

在不同的表中返回最近的日期

从多个表中获取 MAX 日期时间事件,并按 ID 输出最近事件的简单列表

深入理解DOM事件类型系列第三篇——变动事件

如何根据熊猫的时差为用户设置会话

如何根据另一个事件的时间戳按顺序找到最近的事件