LAG本身不判断变更,只搬值;是否变更需用=、!=或IS DISTINCT FROM显式比较,Oracle不支持后者,NULL比较会返回UNKNOWN导致漏判,正确做法是手动展开NULL安全比较,并确保ORDER BY含唯一列防乱序。

LAG 本身不判断变更,只搬值;真正的“是否变更”必须靠你用 =、!= 或 IS DISTINCT FROM 显式写出来。
为什么直接写 LAG(status) != status 可能漏判
Oracle 不支持 IS DISTINCT FROM,所以 NULL 比较会出问题:当上一行 status 是 NULL、当前行是 'active' 时,NULL != 'active' 返回 UNKNOWN,不是 TRUE,整条记录就被过滤掉了。
- 正确做法是手动展开 NULL 安全比较:
(status != LAG(status) OVER (PARTITION BY user_id ORDER BY event_time)) OR (status IS NULL) != (LAG(status) OVER (PARTITION BY user_id ORDER BY event_time) IS NULL) - 如果业务中
status绝对不允许为 NULL,那LAG(status) != status就够用 - 别在
WHERE里直接写这个表达式——窗口函数不能出现在WHERE,得包一层子查询或 CTE
ORDER BY 必须带唯一兜底,否则“变更”可能拉错行
只按 event_time 排序,若同一秒有两条日志,Oracle 不保证它们的相对顺序,LAG() 可能取到同秒内另一条无关记录的 status,导致误标“变更”。
- 务必追加唯一列,例如:
ORDER BY event_time, log_id或ORDER BY event_time, ROWID - 如果表没有自然唯一列,可用
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY event_time) AS rn先生成序号,再基于rn做LAG - 排序字段没索引时,大表上这个查询会触发全排序,响应明显变慢
如何标记“首次从 inactive 切到 active”
这不是单靠 LAG() 能完成的,关键在逻辑封装位置和时间有效性验证。
- 必须同时检查三件事:当前
status = 'active'、上一行LAG(status) = 'inactive'、且上一行时间严格早于当前行(防日志乱序):LAG(event_time) OVER (...) - 漏掉时间比较,就可能把两条并行插入(时间相同)的日志误判为跃迁
- 如果想限制“每个用户只标一次”,得在外层加
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY event_time)并筛rn = 1 -
LAG()的第三参数(默认值)在这里没用——你不需要填默认值,而是要确保比较逻辑覆盖所有 NULL 场景
最易被忽略的一点:变更判断永远依赖窗口定义的稳定性。PARTITION BY 错了,跨用户比;ORDER BY 不唯一,同秒日志乱序;没处理 NULL,空值状态全丢。这些不是“LAG 不好用”,而是它如实反映了你给它的窗口是否可靠。


















