LAG()和LEAD()适合轻量级相邻时间对比,但需ORDER BY唯一排序;非连续日期应JOIN+DATE_SUB对齐;滑动窗口须用ROWS BETWEEN并配合PARTITION BY分组。

用 LAG() 和 LEAD() 做相邻时间窗口对比最直接
想比“昨天 vs 今天”“上周 vs 本周”,不用建多个子查询或临时表,LAG() 和 LEAD() 是最轻量、可读性最好的选择。它们本质是按排序取上/下一行的值,天然适配时间序列对比场景。
常见错误是没写 ORDER BY 或排序字段不唯一:比如只按 date 排序,但同一天有多条记录,结果会随机偏移。必须确保 ORDER BY 能唯一确定行序,例如 ORDER BY date, id 或加 ROW_NUMBER() 辅助。
-
LAG(value, 1)取前 1 行的value,适合“昨日值”“上周值” -
LAG(value, 7)直接跳 7 行,适合“7 天前值”,但前提是数据每天一条且无断更;否则要用日期计算对齐(见下一条) - 如果时间不连续(如跳过周末、节假日),优先用
JOIN或DATE_SUB()关联精确日期,而不是依赖行偏移
用 DATE_SUB() + LEFT JOIN 对齐非连续时间窗口
业务中“上周同日”“去年同期”往往不是简单往前数几行,而是按日历语义对齐。这时靠 LAG() 会出错——比如周五的“上周五”可能因数据缺失变成周四的值。
正确做法是把原表和自身按日期做 LEFT JOIN,用 DATE_SUB(t1.date, INTERVAL 7 DAY) 匹配目标日期。这样哪怕中间缺数据,也能明确知道“该比谁”。
- MySQL / Spark SQL 支持
DATE_SUB(date, INTERVAL N DAY);PostgreSQL 用date - INTERVAL 'N days' - JOIN 条件要写成
t1.date = DATE_SUB(t2.date, INTERVAL 7 DAY),而不是反过来,避免 NULL 漏匹配 - 记得给日期字段建索引,否则自连接性能会随数据量陡增
AVG() 窗口函数里嵌套 ROWS BETWEEN 控制滑动范围
要做“过去 30 天滚动均值”,不能只靠 AVG(value) OVER (ORDER BY date)——默认是累计平均(从第一行算到当前),不是滑动。
必须显式指定窗口帧:ROWS BETWEEN 29 PRECEDING AND CURRENT ROW。数字是“行数”,不是“天数”,所以前提仍是数据按天连续、每日一行。
- 如果某天没数据,那一行就没了,窗口自动变短,结果偏高;想严格按日历滚动,得先用
GENERATE_SERIES(PostgreSQL)或递归 CTE 补全日期 - MySQL 8.0+ 支持
ROWS,但不支持RANGE按值范围滑动(如 “date >= CURRENT_DATE - 30”),别混淆 -
CURRENT ROW包含当前行,UNBOUNDED PRECEDING才是累计,别写反
聚合后套窗口函数容易漏掉 PARTITION BY 导致全局误算
当指标本身需要先分组聚合(比如“每个城市每日订单量”),再算“该城市近 7 日均值”,很多人会写成:
SELECT city, date, AVG(cnt) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) FROM (... GROUP BY city, date) t
这实际是把所有城市的值混在一起排序滑动,结果完全错乱。
- 必须加
PARTITION BY city,让窗口在每个城市内独立计算 - 如果还有渠道、品类等维度,
PARTITION BY要包含全部分组键,和前面GROUP BY一致 - 顺序很重要:
PARTITION BY必须在ORDER BY前面,SQL 标准语法不接受反过来写
时间窗口对比真正难的不是函数怎么写,而是想清楚“比的是什么粒度”和“时间对齐依据是什么”。数据断更、多源异步、业务定义漂移,这些都会让看似正确的 SQL 返回误导性结果。










