LAG()必须配合PARTITION BY和ORDER BY使用,否则会导致跨订单混排;排序需用业务时间如update_time并加唯一二级排序;首行NULL需用COALESCE兜底;WHERE不可直接引用窗口函数,须用子查询或CTE;无效状态须提前清洗。

LAG() 必须配合 PARTITION BY + ORDER BY 才能识别状态跳变
直接写 LAG(status) 而不加 PARTITION BY order_id ORDER BY update_time,会导致跨订单混排——比如把订单 A 的“已发货”和订单 B 的“已下单”强行连成一对,结果完全不可信。状态变化必须限定在单个业务实体(如单个订单、单个用户)内部,且按真实发生顺序排列。
实操建议:
- 排序字段必须是业务时间,不是自增
id:用update_time或event_time,并确认它非空(WHERE update_time IS NOT NULL) - 时间相同时需二级排序:加一个唯一字段如
event_id,避免窗口顺序不确定:ORDER BY update_time, event_id - 别漏掉
PARTITION BY:否则所有记录被当成一个大序列处理,LAG()就失去“上一状态”意义
用 COALESCE() 处理首行 LAG() 返回 NULL 的问题
每个订单的第一条记录没有“前一状态”,LAG(status) 必然返回 NULL。如果直接做 status != LAG(status) 判断,这一行会被过滤掉或逻辑错乱——但业务上它恰恰是状态轨迹的起点。
实操建议:
- 用
COALESCE(LAG(status), 'initial')把首行前值兜底为可识别标记 - 判断变化时写成:
status != COALESCE(LAG(status), 'initial'),而不是status != LAG(status) - 不要用
LAG(status, 1, status)——第三个参数填当前行字段会破坏语义,导致差分为 0
WHERE 子句不能直接引用窗口函数结果
想只查出“状态发生变化”的行,很多人会写 WHERE status != LAG(status) OVER (...),这在绝大多数数据库(PostgreSQL/MySQL 8.0+/SQL Server)中会报错:Window function is not allowed in WHERE clause。因为窗口函数在 SQL 执行顺序中晚于 WHERE。
实操建议:
- 必须用子查询或 CTE 包一层:
SELECT * FROM (SELECT ..., status != COALESCE(LAG(status) OVER (...), 'x') AS changed) t WHERE changed - CTE 更清晰:
WITH with_status_change AS (SELECT ..., COALESCE(LAG(status) OVER (...), 'init') AS prev_status) SELECT * FROM with_status_change WHERE status != prev_status - 别试图在 WHERE 里用别名(如
WHERE status_changed),那只是 SELECT 阶段产物,WHERE 看不见
重复状态或空状态要提前清洗
如果原始数据里存在 status = ''、status IS NULL,或同一订单连续两条都是“已付款”,LAG() 会照常返回这些值,导致“变化检测”误判为跳变或漏判。
实操建议:
- 先过滤掉无效状态:
WHERE status IN ('created', 'paid', 'shipped', 'signed') - 或用
CASE WHEN status IS NULL OR status = '' THEN NULL ELSE status END统一归零再参与比较 - 若需保留空状态但排除干扰,可在 LAG 外层套
NULLIF(status, LAG(status)) IS NOT NULL,比直接等号更鲁棒
状态变化检测真正难的不是写对 LAG(),而是确保输入数据的时间字段可靠、分组粒度准确、空值和重复值有明确定义——这些细节一旦出错,整个轨迹分析就从根上偏了。

















