SQL:根据值的顺序拉取行

SQL: Pull rows based on sequence of values

我需要根据特定序列中存在的某些值来提取数据行。

这是一个数据示例:

Header EventId EventDate
67891882 382 2022-01-21 09:29:50.000
67891882 81 2022-01-21 09:03:23.000
67891882 273 2022-01-21 09:03:51.000
67891882 77 2022-01-21 09:05:58.000
67891882 2 2022-01-21 09:29:48.000

我需要的结果是捕获Header 和EventId=81 的EventDate。进一步的标准包括:

因此在上面的示例中,EventID 81 和 EventDate 2022-01-21 09:03:23.000 符合我要提取的行,因为 273 不在异常列表中。

ATTEMPT: 我尝试了以下查询

SELECT *
FROM #Table
WHERE EventDate BETWEEN (SELECT EventDate
                         FROM #Table
                         WHERE EventId = 81)
                    AND (SELECT eventdate
                         FROM #Table
                         WHERE EventId = 77)
    AND EventId NOT IN (60, 72, 73, 74, 75, 76, 83, 85, 86, 87, 103, 154, 166, 197, 199)
ORDER BY 3

但我立即面临这样一个事实,即我的子查询 return 不止一个结果,所以这行不通(我用它来测试一个单独的 Header # 示例,效果很好).所以现在我不太确定如何进行。我不愿意认为我会被迫使用 CURSOR,主要是因为我的源数据由 2.66 亿行组成。

我之前也曾尝试使用 LAG() 函数来找到我的“起点”,但是一旦请求开始变得越来越复杂(添加排除列表作为以及在 81 和 77 之间可能有 1 或 40 行的事实。


我该如何处理?下面是一些可以使用的示例数据。 Header 可以被认为是父键,与任意数量的 EventID(代表特定操作)和 EventDate 相关联,事件发生时间:

create table #data (header int, eventid int, eventdate datetime)

insert into #data 
values
('62252595',    '22',   '5/23/2021  12:34:02 PM'),
('62252595',    '81',   '5/23/2021  12:34:03 PM'),
('62252595',    '29',   '5/23/2021  12:34:12 PM'),
('62252595',    '40',   '5/23/2021  12:34:27 PM'),
('62252595',    '22',   '5/23/2021  12:35:02 PM'),
('62252595',    '22',   '5/23/2021  12:36:12 PM'),
('62252595',    '37',   '5/23/2021  12:36:36 PM'),
('62252595',    '77',   '5/23/2021  12:37:04 PM'),
('62252595',    '6',    '5/23/2021  12:37:52 PM'),
('63252595',    '39',   '5/23/2021  12:38:01 PM'),
('63252595',    '81',   '5/23/2021  12:38:04 PM'),
('63252595',    '37',   '5/23/2021  12:38:06 PM'),
('63252595',    '21',   '5/23/2021  12:38:09 PM'),
('63252595',    '75',   '5/23/2021  12:38:10 PM'),
('63252595',    '77',   '5/23/2021  12:38:12 PM'),
('64252595',    '29',   '5/23/2021  12:38:15 PM'),
('64252595',    '26',   '5/23/2021  12:38:18 PM'),
('64252595',    '81',   '5/23/2021  12:38:20 PM'),
('64252595',    '40',   '5/23/2021  12:38:21 PM'),
('64252595',    '81',   '5/23/2021  12:38:24 PM'),
('64252595',    '83',   '5/23/2021  12:39:06 PM'),
('64252595',    '77',   '5/23/2021  12:39:07 PM'),
('65252595',    '41',   '5/23/2021  12:39:12 PM'),
('65252595',    '81',   '5/23/2021  12:39:16 PM'),
('65252595',    '37',   '5/23/2021  12:39:20 PM'),
('65252595',    '18',   '5/23/2021  12:39:56 PM'),
('65252595',    '18',   '5/23/2021  12:40:03 PM'),
('65252595',    '77',   '5/23/2021  12:40:15 PM'),
('65252595',    '36',   '5/23/2021  12:40:46 PM'),
('65252595',    '77',   '5/23/2021  12:40:53 PM')

预期结果: 从这个 #Data table,我希望看到的结果是:

Header EventId EventDate
62252595 81 5/23/2021 12:34:03 PM
65252595 81 5/23/2021 12:39:16 PM

Header #'s 63252595 和 64252595 不符合条件,因为在 81 的第一个实例和 77 的第一个实例之间(按 EventDate 按 Header 顺序分区),在 5/23/2021 12:38:10 PM 处存在一个 75 和一个 83分别位于 5/23/2021 12:39:06 PM(均在排除列表中)。我希望这能消除一些困惑。


编辑: 经过一番思考,我想知道是否可以使用 CASE 表达式来简化它。使用上面 #Data table 中的示例数据,我编写了这个查询:

select *
from (
    select * from (
        select *, id=case when EventId = 81 then 1 
                            when EventId = 77 then 2
                            when EventId in (60, 72, 73, 74, 75, 76, 83, 85, 86, 87, 103, 154, 166, 197, 199) then 5 else 0 end
        from #data) a
    where id <> 0)b
    order by 3

它所做的是过滤掉所有 'allowable' 事件并进行过滤,以便我可以过滤以仅查看未受阻的事件,其中 id=1 然后是 2。什么我还不确定如何让它只显示 id=1 的条目和后面的 2.

查看示例数据的实际预期结果会很方便,所以我实际上不知道这是否正确,看起来您只需要计算每个 header 的日期范围:

with h as (
  select *, 
    Min(case when eventid=81 then eventdate end) over(partition by header) Sdate, 
    Max(case when eventid=77 then eventdate end) over(partition by header) Edate
  from #data
)
select header, eventId, EventDate
from h
where eventdate between SDate and EDate
and EventId not in(60, 72, 73, 74, 75, 76, 83, 85, 86, 87, 103, 154, 166, 197, 199)
order by eventdate;

我假设开始事件 (81) 总是从该行开始一个新的“帧”,而结束事件 (77) 总是从下一行开始一个新的帧。

我还假设您只对同时存在开始和结束事件的帧感兴趣,并且该帧不包含异常事件(我将只使用 00作为随机允许事件,199 作为唯一例外事件).

例如...

[81,00,81,00,77,00,81,199,77]
=> frame 0 = [81,00]
=> frame 1 = [81,00,77]
=> frame 2 = [00]
=> frame 3 = [81,199,77]

在该示例中,只有第 2 帧的开始事件会被 return 编辑(其他缺少开始 and/or 结束事件,或包含异常事件)。

WITH
  frame_start AS
(
  SELECT
    *,
    CASE
      WHEN
        81 = eventid
      OR
        77 = LAG(eventid) OVER (PARTITION BY header ORDER BY eventdate)
      THEN
        1
      ELSE
        0
    END
      AS new_frame
  FROM
    #data
),
  framed AS
(
  SELECT
    *,
    SUM(new_frame) OVER (PARTITION BY header ORDER BY eventdate) AS frame_id
  FROM
    frame_start
)
SELECT
  header, MIN(eventdate)
FROM
  framed
GROUP BY
  header, frame_id
HAVING
  SUM(CASE WHEN eventid IN (81,77) THEN 1 ELSE 0 END) = 2
  AND
  MAX(CASE WHEN eventid IN (199, etc) THEN 1 ELSE 0 END) = 0

演示:https://dbfiddle.uk/?rdbms=sqlserver_2019&fiddle=d54493c87629e3e59759ac9d119ec6ad


解释:

第一个 CTE 添加了一个名为 new_frame 的列。

  • 1 = 当前行是 81,或者前一行是 77
  • 0 = 其他一切

这标志着每个新帧的开始(如此处顶部所述)。

下一个 CTE 通过按日期时间顺序对 new_frame 进行累加,为每一帧中的每一行分配一个 id。 id 从 0 开始,然后在每一行上增加该行的 new_frame 值(如果 new_frame=0,保持与前一行相同的 id,如果 new_frame=1 增加id 加 1).

此时 header 的行被分解为帧(如此处顶部所述)。

最终查询按帧分组,然后使用 HAVING 子句过滤结果。第一个检查是框架中具有 81 或 77 的行数总计必须为 2。第二个检查是框架中的任何行都不能有异常事件。如果所有检查都通过,return 帧中的最小时间戳,根据定义,它来自帧中的第一行。