LAG()在旧版数据库中不可用,且在排序字段重复时行为不可靠;关联子查询通过WHERE+ORDER BY DESC+LIMIT 1模拟其功能。

为什么不能直接用 LAG()?
在 MySQL 5.7 或更早版本、SQL Server 2008 及之前、某些嵌入式数据库(如 SQLite 3.27 以前)中,LAG() 根本不可用。即使支持,如果排序依据字段存在重复值且无唯一键兜底,LAG() 的行为也可能非预期——它按窗口定义的逻辑顺序取“上一行”,但底层物理顺序不保证稳定。
关联子查询模拟 LAG 的核心写法
本质是:对当前行 t1,在相同表的别名 t2 中找满足“排序值严格小于 t1 的值,且尽可能大”的那条记录。关键在 ORDER BY ... DESC LIMIT 1 和关联条件。
假设有一张订单表 orders,含 id(主键)、created_at(时间)、amount(金额),想查每笔订单的前一笔订单金额:
SELECT t1.id, t1.created_at, t1.amount, (SELECT t2.amount FROM orders t2 WHERE t2.created_at < t1.created_at ORDER BY t2.created_at DESC LIMIT 1) AS prev_amount FROM orders t1;
WHERE t2.created_at 确保只查“更早”的行-
ORDER BY t2.created_at DESC LIMIT 1拿最近的那一个(即上一行) - 若
created_at有重复,结果不确定;此时必须加入唯一字段(如id)辅助排序:ORDER BY t2.created_at DESC, t2.id DESC - 没有索引时性能极差——务必在
(created_at)或(created_at, id)上建复合索引
处理 NULL 和边界情况
第一行没有“上一行”,子查询返回 NULL,这是正确行为。但若需默认值(比如设为 0),不能用 COALESCE(..., 0) 包裹整个子查询(部分旧版 MySQL 不允许),应改用:
SELECT
t1.id,
t1.created_at,
t1.amount,
IFNULL((SELECT t2.amount
FROM orders t2
WHERE t2.created_at < t1.created_at
ORDER BY t2.created_at DESC, t2.id DESC
LIMIT 1), 0) AS prev_amount
FROM orders t1;
-
IFNULL()比COALESCE()在 MySQL 中更稳妥(尤其子查询场景) - 若排序字段允许
NULL,t2.created_at 会自动跳过 <code>NULL行,无需额外判断 - SQL Server 要用
ISNULL(),PostgreSQL 用COALESCE()即可
比窗口函数慢多少?怎么预估
关联子查询是 N × log(N) 级别:对每一行都触发一次独立的子查询,每次都要走索引查找+排序。而原生 LAG() 是单次全表扫描 + 内存排序,O(N)。
- 1 万行数据,有合适索引时延迟通常在 50–200ms;10 万行可能升至 2–5 秒
- 执行计划里若看到
DEPENDENT SUBQUERY,就是它在拖慢速度 - 真要处理大数据量,优先升级数据库版本或改用临时表+自连接(但代码更重)
真正容易被忽略的是排序字段的稳定性——哪怕加了索引,只要业务上允许 created_at 重复且无其他唯一约束,prev_amount 就可能每次执行结果不同。这时候必须把主键或时间戳微秒级字段一起塞进 ORDER BY。

















