lag函数必须显式指定order by时间字段(如dt),配合partition by防跨组混算;月环比需先聚合再窗口计算,首行null需用nullif避免除零,同比应补全时间序列而非硬写lag(...,12)。

LAG 函数怎么写才能正确取上期值
核心问题不是 LAG 会不会用,而是它默认按什么顺序取、取错行就全错。必须显式指定 ORDER BY,且排序字段得是时间维度(比如 dt 或 month),不能依赖表里物理顺序。
常见错误现象:LAG(sales) OVER () 没写 ORDER BY,结果每行的“上月值”随机漂移;或者用 id 排序,但 id 和业务时间不一致,导致同比算成隔壁门店的数据。
- 必须写成
LAG(sales) OVER (PARTITION BY region ORDER BY dt),PARTITION BY防止跨区域混算 - 如果原始数据是日粒度但要算月环比,先按月聚合再用
LAG,别在日级上直接LAG后再分组——那样会多出几十倍中间行 - PostgreSQL 和 ClickHouse 支持
LAG(value, 2)取前两期,但 MySQL 8.0+ 才支持偏移量参数,旧版只能嵌套窗口函数
同比和环比的分母为什么老是 NULL
LAG 在首行没上期值,自然返回 NULL;如果没处理,整个增长率表达式就会变成 NULL。这不是 bug,是设计如此——但业务报表里不能留空。
使用场景:月初跑上月环比时,1月数据没有“上月”,但你仍需显示 0% 或 “-” 而非空白。
- 用
COALESCE(LAG(sales), 0)简单兜底?危险!分母为 0 会触发除零错误,得拆开判断 - 安全写法是:
CASE WHEN LAG(sales) = 0 THEN NULL ELSE (sales - LAG(sales)) / LAG(sales) END - 更稳妥的:先用
NULLIF(LAG(sales), 0)把分母 0 转成NULL,再整体除,避免报错
按年同比 vs 按月环比,窗口定义差在哪
同比看的是“去年同月”,环比看的是“上一周期”,两者时间跨度不同,不能共用一个 LAG 偏移量。
参数差异明显:LAG(sales, 1) 是环比(上月),LAG(sales, 12) 是年同比(去年同月)——但前提是数据是月粒度且连续无断层。
- 如果数据缺 2023-02,则
LAG(sales, 12)在 2024-02 取到的是 2023-01,不是真正同比,得先补全时间序列 - ClickHouse 里可用
toMonth()+toYear()构造组合键做PARTITION BY,再对dt排序,比硬写LAG(..., 12)更可靠 - MySQL 8.0 不支持
LAG的动态偏移,想实现同比就得用自连接或子查询,性能差一截
聚合后计算 vs 计算后聚合,哪个快
先 GROUP BY month, region 再套窗口函数,比在明细行上窗口再 GROUP BY 快得多——尤其当明细是千万级订单时。
性能影响显著:后者会让 LAG 在每条订单行上执行,然后才合到月维度;前者只在几百行聚合结果上跑窗口,内存和 CPU 都省 90% 以上。
- 错误写法:
SELECT month, LAG(SUM(sales)) OVER (...) FROM orders GROUP BY month—— 语法都错,GROUP BY外不能直接引用未聚合字段 - 正确链路:CTE 先聚合 → 主查询对 CTE 结果用
LAG→ 最终 SELECT 增长率 - Spark SQL 或 Trino 中,如果用了
approx_percentile类估算函数,别在窗口里嵌套它——不支持且报错,得先估算再窗口
最易被忽略的一点:时间字段类型必须是真正的日期型(DATE 或 TIMESTAMP),别用字符串存 “202401”。否则 ORDER BY 会按字典序排,“202412” 会排在 “202402” 前面,LAG 就取反了。










