取 ConfirmationStarted 之前最近的一条 Closed

问题描述

数据库表 mytable 存储多个 ID 在不同时间点 CreatedAt 的状态 NewStatus,每个 ID 一定有一个 ConfirmationStarted 和一个或多个 Closed 状态。现在要在每个 ID 内,找到 ConfirmationStarted 之前的所有的 Closed 中,离 ConfirmationStarted 最近的那条记录,取出记录的 ID 和时间字段。

源数据

CreatedAt

ID

NewStatus

2022-05-25 23:17:44.000

147

Active

2022-05-28 05:59:02.000

147

Closed

2022-05-30 20:48:53.000

147

Active

2022-06-18 05:59:01.000

147

Closed

2022-06-21 20:09:48.000

147

Active

2022-06-25 05:59:01.000

147

Closed

2022-07-13 00:02:47.000

147

ConfirmationStarted

2022-07-15 15:33:30.000

147

ConfirmationDone

2022-08-25 05:59:01.000

147

Closed

2023-03-08 13:34:57.000

1645

Draft

2023-03-22 19:58:51.000

1645

Active

2023-04-29 05:59:02.000

1645

Closed

2023-05-08 14:50:29.000

1645

Awarded

2023-05-08 14:53:34.000

1645

ConfirmationStarted

2023-05-08 17:53:55.000

1645

ConfirmationDone

期望结果

ID

CreatedAt

147

2022-06-25 05:59:01.000

1645

2023-04-29 05:59:02.000

以 ID=147 为例:

ConfirmationStarted 出现在 2022-07-13,此前的三条 Closed 分别发生在 05-28、06-18、06-25,离它最近的一条是 2022-06-25 05:59:01,正是期望结果中的时间。

ID=1645 的 ConfirmationStarted 出现在 2023-05-08 14:53:34,此前只有一条 Closed(2023-04-29 05:59:02),所以结果取它。

SQLazy 分步实现

核心思路:把每个 ID 的记录按时间排序后,用 segment 在出现 ConfirmationStarted 的地方切段,第一个 ConfirmationStarted 之前的记录自然落在 seg=1;再过滤出 seg=1 且状态为 Closed 的记录,最后按 ID 汇总取 CreatedAt 的最大值,即离 ConfirmationStarted 最近的一条 Closed。

【点击在线运行本例】

下面逐一解释这些步骤。

Name

Anchor

Statement

t1

mytable

sort ID, CreatedAt asc

t2


segment condition (NewStatus = "ConfirmationStarted") partition ID as seg

t3


filter (NewStatus = "Closed" and seg = 1)

t4


summarize CreatedAt max as CreatedAt; group ID

第 1 步:按 ID 和时间升序排序

sort ID, CreatedAt asc

保证每个 ID 内部的记录按时间先后排列,后续分段和取 "最近" 才有依据。

Picture3png

第 2 步:遇到 ConfirmationStarted 就新开一段

segment condition (NewStatus = "ConfirmationStarted") partition ID as seg

这是核心的一步。segment 按 partition ID 在每个 ID 内独立分段;分段条件指明:每当遇到 NewStatus 为 ConfirmationStarted 的记录,就开启新的一段并编号为 seg。这样,第一个 ConfirmationStarted 之前的所有记录都落在 seg=1,ConfirmationStarted 本身及之后的记录 seg 依次递增。一条语句就把 "目标状态之前" 的区间划了出来。

Picture4png

第 3 步:过滤出目标记录

filter (NewStatus = "Closed" and seg = 1)

只保留 seg=1(第一个 ConfirmationStarted 之前)且状态为 Closed 的记录,这些就是每个 ID 在 ConfirmationStarted 之前的全部 Closed。

Picture5png

第 4 步:按 ID 汇总取最近的 Closed 时间

summarize CreatedAt max as CreatedAt; group ID

在每个 ID 内对 CreatedAt 取最大值。因为前面已经按时间升序排列,最大值就是离 ConfirmationStarted 最近的那条 Closed。summarize 直接用 "按 ID 分组、取 CreatedAt 最大" 的业务语义描述聚合,不需要手工编写窗口函数。

Picture6png

编译生成 SQL

确认上述 4 步逻辑后,SQLazy 编译器自动生成原生 SQL(这里是 Oracle 语法):

WITH t2 AS (
        SELECT CreatedAt, ID, NewStatus
            , 1 + SUM(CASE
                WHEN (NewStatus = 'ConfirmationStarted') THEN 1
                ELSE 0
            END) OVER (PARTITION BY ID ORDER BY ID ASC, CreatedAt ASC ROWS UNBOUNDED PRECEDING) AS seg
        FROM mytable
    )
SELECT ID, MAX(CreatedAt) AS CreatedAt
FROM (
    SELECT CreatedAt, ID, NewStatus, seg
    FROM t2
    WHERE (NewStatus = 'Closed'
        AND seg = 1)
) t_3
GROUP BY ID
ORDER BY ID

SQLazy 让你用业务语言描述逻辑,而不是用 SQL 语法写嵌套查询。这类 "按事件切段、再从指定区段取记录" 的问题,核心是给事件流打上区段标记:segment 条件分段功能直接用 "遇到 ConfirmationStarted 就切段" 描述业务语义,partition 让分段在每个 ID 内独立进行。先分段、再过滤、后汇总的分步计算,让每一步的中间结果都可以独立验证;summarize 用 "按 ID 分组、取最大时间" 这样直白的语句完成聚合,编译器自动生成可运行的 SQL。

官方链接

SQLazy 在线体验:sqlazy.com(免费,无需注册)

SQLazy 项目仓库:github.com/SPLWare/SQLazy