LAG和LEAD是最直接的跨行取值方案,因窗口函数天然支持按ORDER BY排序访问相邻行:LAG取前一行、LEAD取后一行,无需自连接;必须显式指定ORDER BY,否则“前后行”无定义,结果不可预测。

为什么 LAG 和 LEAD 是最直接的跨行取值方案
窗口函数天然支持按排序顺序访问相邻行,LAG 取前一行、LEAD 取后一行,完全绕开自连接。关键在于必须显式指定 ORDER BY——没有排序就没有“上一行”的定义,否则结果不可预测。
常见错误是漏写 ORDER BY 或在 PARTITION BY 后误以为组内自动有序;实际每组仍需独立排序。
-
LAG(col, 1, 0)表示取前 1 行的col值,若不存在则用0填充 - 偏移量支持变量,比如
LAG(sales, day_diff)(PostgreSQL 支持,MySQL 8.0+ 不支持动态偏移) - 性能上比自连接快一个数量级,因为只扫描一次表,不产生笛卡尔积
计算同比/环比时 LAG 配合算术运算就够了
比如月度销售额环比增长:不需要把当前月和上月数据拼成两列再减,直接用 LAG 拿上月值参与计算即可。
SELECT month, sales, sales - LAG(sales) OVER (ORDER BY month) AS mom_change, ROUND((sales * 100.0 / LAG(sales) OVER (ORDER BY month)) - 100, 2) AS mom_pct FROM monthly_sales;
注意点:
-
LAG(sales)默认偏移 1,等价于LAG(sales, 1) - 首行的
LAG返回NULL,除零或空值传播会污染整列,建议用COALESCE(LAG(...), 0)或保留NULL更语义清晰 - 如果按年分区计算年同比,改用
PARTITION BY EXTRACT(YEAR FROM date) ORDER BY month不起作用——必须把年份也纳入ORDER BY才能跨年对齐
累计类计算优先用 SUM() OVER (ORDER BY ... ROWS BETWEEN ...)
需要“截至当前行”的累加、移动平均、滚动计数时,ROWS BETWEEN 显式控制窗口帧,比依赖默认的 RANGE 更可靠,尤其当排序字段有重复值时。
- 默认
SUM(x) OVER (ORDER BY t)是RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,遇到相同t值会把所有同值行全包进来 - 要严格按物理行序滚动 7 天,得写
SUM(x) OVER (ORDER BY t ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) - PostgreSQL 支持
INTERVAL类型帧(如ORDER BY ts RANGE BETWEEN '7 days' PRECEDING AND CURRENT ROW),但 MySQL 和 SQL Server 不支持
遇到 Window function is not allowed in WHERE/HAVING 怎么办
窗口函数不能出现在 WHERE 或 HAVING 中,这是语法硬限制。想筛出“环比增长 > 10%”的记录,必须用子查询或 CTE 包一层。
WITH ranked AS (
SELECT *,
sales / LAG(sales) OVER (ORDER BY month) - 1 AS growth_rate
FROM monthly_sales
)
SELECT * FROM ranked WHERE growth_rate > 0.1;容易踩的坑:
- 别试图用
HAVING对窗口函数结果过滤——它只作用于GROUP BY聚合结果,而窗口函数在聚合之后执行 - CTE 不是万能的,某些旧版 MySQL(FROM (SELECT ...) AS t)
- 嵌套层级多了可能影响可读性,但比起自连接,维护成本依然低得多
真正麻烦的是需要跨多维度排序再取值的场景,比如“每个品类下价格第二高的商品”——这时 ROW_NUMBER() + PARTITION BY 是解法,但要注意 RANK() 和 DENSE_RANK() 在并列时的行为差异,这个细节常被忽略。

















