同组内相邻行比对的典型场景是按字段分组后对组内排序连续两行做差值等操作;常用LAG()/LEAD(),旧版数据库不支持时需用相关子查询模拟,核心是找同组内排序字段严格小于当前行且差值最小的记录。

什么是同组内相邻行比对的典型场景
就是按某个字段(比如 user_id)分组后,对每组内按时间/序号排序的连续两行做差值、判断变化、标记首次发生等操作。常见于用户行为序列分析、设备状态跃变检测、订单状态流转校验。直接用 LAG() 或 LEAD() 最省事,但某些旧版数据库(如 MySQL 5.7 或早期 SQL Server)不支持窗口函数,这时才需要相关子查询兜底。
用相关子查询模拟 LAG:取前一行的某个字段
核心思路是:对当前行 t1,在同组内找一个满足「排序字段严格小于 t1.sort_col」且「差值最小」的记录。必须同时满足分组条件和排序约束,否则会跨组拉数据。
假设表 events 有字段 user_id、event_time、status,要获取每条记录前一次的状态:
SELECT t1.user_id,
t1.event_time,
t1.status,
(SELECT t2.status
FROM events t2
WHERE t2.user_id = t1.user_id
AND t2.event_time < t1.event_time
ORDER BY t2.event_time DESC
LIMIT 1) AS prev_status
FROM events t1;
- WHERE 中的
t2.user_id = t1.user_id是分组前提,漏掉就会变成全局查找 t2.event_time 保证只看“前面”的行,不能写成 <code>,否则可能取到自己-
ORDER BY ... DESC LIMIT 1是取“最近的前一行”,不是任意前一行;MySQL 5.7 支持LIMIT,但 SQLite 需用FETCH FIRST 1 ROW ONLY,SQL Server 要用TOP 1 - 性能上,
user_id + event_time必须有联合索引,否则全表扫描极慢
比对相邻行并标记变化:避免 NULL 干扰
直接用 prev_status != status 会因 prev_status 为 NULL(首行无前驱)导致整行被过滤或判断失败。必须显式处理 NULL。
例如标记状态是否发生变化:
SELECT user_id, event_time, status,
CASE
WHEN prev_status IS NULL THEN 'first'
WHEN prev_status != status THEN 'changed'
ELSE 'same'
END AS change_flag
FROM (
SELECT t1.*,
(SELECT t2.status
FROM events t2
WHERE t2.user_id = t1.user_id
AND t2.event_time < t1.event_time
ORDER BY t2.event_time DESC
LIMIT 1) AS prev_status
FROM events t1
) t;
- 相关子查询结果默认为 NULL,不能假设它总存在
- 比较时用
!=而非更通用,但二者在标准 SQL 中等价 - 如果业务要求区分“从 NULL 到某值”和“值变更为 NULL”,需额外加判空逻辑,比如
COALESCE(prev_status, '_null_') != COALESCE(status, '_null_')
为什么不用 JOIN 模拟?相关子查询的不可替代性
有人尝试用自连接:LEFT JOIN events t2 ON t1.user_id = t2.user_id AND t2.event_time ,再配合 <code>GROUP BY 和 MAX(t2.event_time) 取最近时间——这理论上可行,但实际极易出错:
- 若同一组内存在相同
event_time,MAX(t2.event_time)会命中多行,聚合后状态值不确定(除非加唯一约束) - JOIN 后数据量爆炸,尤其大表+高重复度时间戳时,性能远差于相关子查询的单次最值查找
- 相关子查询天然“每行独立执行”,语义清晰;而 JOIN + 聚合容易混淆作用域,调试困难
- 当需要取前两行、前三行时,相关子查询只需改
LIMIT 2并包装成 JSON/数组(如 PostgreSQL 的ARRAY()),JOIN 方案则需复杂嵌套
真正难的不是写出语法,而是确保分组键、排序键、NULL 边界、索引覆盖这四点全部对齐——少一个,结果就错得悄无声息。

















