同比需用lag(value, 12)并确保按year_month升序排列,环比用lag(value, 1)且须先聚合到对应周期;注意排序、补全数据、null处理及索引优化。

怎么用 LAG 算同比(和去年同月比)
同比本质是「当前行」和「往前偏移 N 行」的值做差或比,关键是算准偏移量。比如按月分组后,2024-05 的同比要拉 2023-05,不是简单 LAG(value, 1)——那只是上个月。得先保证数据已按 year_month 排序,再用 LAG(value, 12)(月粒度)或 LAG(value, 4)(季度粒度)。
常见错误:没排序就直接 LAG,结果错位;或者用 ORDER BY created_at 但字段含时分秒,导致同月多行顺序不确定。
- 必须显式
ORDER BY year_month ASC(推荐用整数年月如202405,避免字符串排序出错) - 如果原始数据缺某些月份(比如 2023-02 没记录),
LAG(value, 12)会跳过空缺,直接取更早的非空值——这不是 bug,是预期行为,需提前补全月份 - 注意时区:若
year_month是从created_at提取,确保提取逻辑统一(例如都用TO_CHAR(created_at AT TIME ZONE 'Asia/Shanghai', 'YYYYMM'))
怎么用 LEAD 算环比(和上期比)
环比更简单,就是和紧邻上一期比,通常用 LAG(value, 1) 就够了。但“上期”定义必须明确:是按时间顺序的上一行,不是按业务逻辑的“上一个自然周期”。比如用户活跃数按天统计,LAG(active_users, 1) 就是昨天;但如果是周报,就得先聚合到周粒度再 LAG,否则会把周一到周日七行都当成“本期”。
容易踩的坑是没做预聚合。直接在明细表上跑 LAG,可能得到“同一周内某天 vs 前一天”,而不是“本周 vs 上周”。
- 先
GROUP BY week_start_date(或用DATE_TRUNC('week', dt)),再开窗 -
LEAD其实不常用在环比——它往“后”取,而环比需要“往前”参照,坚持用LAG更直观 - 当存在多维度分组(比如按
region和product),PARTITION BY region, product ORDER BY month必须写全,漏一个就会跨区混算
NULL 怎么处理?同比/环比结果经常是 NULL
只要第一行、或前 N 行没数据,LAG/LEAD 就返回 NULL——这是 SQL 标准行为,不是函数写错了。但业务报表里不能留一堆 NULL,得主动处理。
别用 COALESCE(LAG(x, 12), 0) 直接填 0,同比变成 “本月 100 vs 去年同月 0”,实际可能是去年没数据,不是为 0。
- 更安全的是用
CASE WHEN LAG(x, 12) IS NOT NULL THEN (x - LAG(x, 12)) / NULLIF(LAG(x, 12), 0) END - 或者加一列标记有效性:
LAG(x, 12) IS NOT NULL AS has_yoy_base,让下游自己决定如何过滤或提示 - PostgreSQL 支持
LAG(x, 12, 0)第三个参数设默认值,但 MySQL 不支持,跨库迁移时要注意
性能为什么突然变慢?窗口函数拖垮查询
LAG 本身不慢,慢是因为它强制要求完整排序 + 分区扫描。如果数据量大、分区键选择不当,或者没索引支撑 ORDER BY 字段,执行计划会退化成 filesort 或临时表。
典型症状:加了 LAG 后查询从 200ms 涨到 12s,EXPLAIN 显示 Using temporary; Using filesort。
- 确保
PARTITION BY a, b ORDER BY c这组字段有联合索引,顺序严格匹配(如INDEX(a, b, c)) - 避免在
ORDER BY里用函数,比如ORDER BY YEAR(dt), MONTH(dt)会让索引失效,改用预计算列year_month INT并建索引 - 如果只查最近 12 个月,先
WHERE year_month >= 202306再开窗,别让窗口函数扫全表
复杂点在于:窗口函数的优化高度依赖执行引擎对排序和分区的物化策略,不同数据库差异大——MySQL 8.0 对 PARTITION BY 的并行支持弱,而 ClickHouse 用 neighbor 函数替代 LAG 会快一个数量级。别只盯着语法,得看执行计划里到底扫了多少行。










