LAG/LEAD必须在SELECT中作用于原始行级数据,不能用于GROUP BY后的聚合结果;正确做法是先用窗口函数计算差值,再聚合或过滤。

不能在GROUP BY之后直接用LAG/LEAD计算跨行差值——窗口函数必须作用于原始行级数据,而非聚合后的结果集。
LAG/LEAD 必须出现在 SELECT 中,不能用于 GROUP BY 后的查询层级
常见错误是先 GROUP BY 汇总出月度销售额,再试图对汇总结果套 LAG(sales)。这在语法上非法:SQL 执行顺序是 FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY,而窗口函数只能在 SELECT 和 ORDER BY 阶段生效,此时分组已压缩为单行,LAG 已无“前一行”可取。
- 正确路径只有一条:先用
LAG()在原始明细行上算出差值(保持粒度不变),再按需对带差值的中间结果做 GROUP BY 或过滤 - 例如想看“每个用户每日登录时长相比昨日的增长”,必须基于原始日志行计算
duration_sec - LAG(duration_sec) OVER (PARTITION BY user_id ORDER BY event_time),而不是先 GROUP BY user_id, date::date - 如果硬要聚合后差分,得用子查询或 CTE 把窗口计算结果当成新表再处理
PARTITION BY 字段必须和业务分组维度严格一致
差值是否“组内有效”,全看 PARTITION BY 写得对不对。写错会导致 A 用户的最后一行和 B 用户的第一行相减,结果完全失真。
- 若业务目标是“每个产品线内各月环比”,
PARTITION BY product_line是必须的;漏掉就变成全表排序后跨产品线取值 - 若原始数据含多级分组(如 region → city → store),而你只想按 city 算差值,
PARTITION BY city就够了,别把 region 也塞进去——否则同一 region 下不同 city 会被强行隔离 - MySQL 8.0+、PostgreSQL、ClickHouse 均要求显式声明
PARTITION BY,不写等价于全表一个组,风险极高
ORDER BY 必须确定、稳定,且不能缺失
LAG() 和 LEAD() 的行为完全依赖排序结果。ORDER BY 缺失会报错;排序不稳定(如仅用 ORDER BY status,而 status 大量重复)会导致每次执行结果不一致。
- 时间类字段优先用
ORDER BY event_time, id,id 作为次要排序键解决并列问题 - 避免在
ORDER BY里用高开销表达式,比如ORDER BY parse_json(metadata)->>'ts',可能拖慢整个窗口计算 - 日期归一化要提前做:日粒度数据算月环比,先用
DATE_TRUNC('month', event_time)或TO_CHAR(event_time, 'YYYY-MM')聚合成月,再排序 - 首行
LAG()返回NULL,除法或比较前务必用COALESCE(LAG(), 0)或NULLIF()处理,否则/ NULL得到 NULL,> 0判定失败
复杂差值逻辑需拆解为多步:先取值,再运算
LAG 和 LEAD 本身只是取相邻行某个字段的值,不是计算器。想实现“上期 × 1.2”或“前三行平均值”,不能指望它们一步到位。
- 要算“相比上月增长 20%”,写成
sales / NULLIF(LAG(sales) OVER (...), 0) >= 1.2,而不是试图让 LAG 直接返回乘完的结果 - 要算“过去 7 天滚动平均”,得用
AVG(sales) OVER (PARTITION BY ... ORDER BY day ROWS BETWEEN 6 PRECEDING AND CURRENT ROW),不是LAG(sales, 7) - ClickHouse 用户注意:
lagInFrame()是专用优化函数,比通用LAG()更快,但只支持 21.4+ 版本;旧版只能靠数组模拟,性能差一个数量级
真正容易被忽略的是:差值计算永远绑定在原始行粒度上。哪怕你最终只想要每个分组的最大差值,也得先在每一行算出自己的差值,再用 MAX() 聚合——跳过这一步,就等于绕开了窗口函数的设计前提。











