同比需先按年月分组聚合再用LAG(12)对齐同月,环比须用YEARWEEK或ISO周函数统一周期,增长率须用CASE或NULLIF处理分母为0或NULL,且窗口前必须加时间过滤以提升性能。

同比计算总出错:LAG() 用错列或没分区
同比不是简单拿今年减去年,而是要对齐相同时间段(比如 2024-03 和 2023-03),窗口函数必须按月(或按日/季)聚合后再位移。常见错误是直接对原始明细行用 LAG(sales, 12),但没先按 YEAR(order_date), MONTH(order_date) 分组汇总,导致位移错乱。
正确做法是两步:先用 GROUP BY YEAR(order_date), MONTH(order_date) 算出每月销售额;再套一层窗口,按年月升序排列,用 LAG(sum_sales, 12) 拉上一年同月值。注意 ORDER BY 必须是可排序的连续时间键(比如 YEAR(order_date) * 100 + MONTH(order_date)),不能只写 order_date —— 否则同月多行会打乱顺序。
-
LAG()的第二个参数填 12 还是 1,取决于你分组粒度:月级同比填 12,日级同比填 365(需考虑闰年,更稳用DATE_SUB()关联) - 如果数据跨多年但某月缺失(如 2023-02 没数据),
LAG()会跳过空值拉到更早的非空值,得用LAG(sum_sales, 12) OVER (PARTITION BY ... ORDER BY ym)配合完整时间维度补零 - MySQL 8.0+、PostgreSQL、SQL Server 2012+ 支持,但 SQLite 不支持
LAG()
环比老是算成“昨天比今天”:日期截断不统一
环比本质是相邻周期比较,但原始 order_date 常含时分秒,直接 LAG(sales) 会把同一日多笔订单互相比较。必须先用日期函数归一化到目标粒度——比如环比按周算,就得用 YEARWEEK(order_date, 1)(MySQL)或 TO_CHAR(order_date, 'IYYY-IW')(PostgreSQL)生成周键。
别信 DATE(order_date) 或 CAST(order_date AS DATE) 就够用:它只去时分秒,没解决“周一到周日怎么对齐”。不同数据库周起始日不同(MySQL 默认周日,PostgreSQL 默认周一),硬写 WEEKDAY() 调整容易翻车。
- MySQL 推荐用
YEARWEEK(order_date, 1)(周一为周首);PostgreSQL 用EXTRACT(ISOWEEK FROM order_date)配合EXTRACT(ISOYEAR FROM order_date) - 用
LAG(sum_sales)时,ORDER BY必须和分组键一致,否则窗口排序和分组错位,环比值全乱 - 如果某周期无数据,
LAG()返回 NULL,除法会变 NULL —— 计算增长率前务必加CASE WHEN prev_value IS NULL OR prev_value = 0 THEN NULL ELSE (cur_value - prev_value) / prev_value END
增长率除零或 NULL 泛滥:没处理分母边界
同比/环比增长率公式是 (current - previous) / previous,但 previous 为 0 或 NULL 时,结果不是报错就是 NULL,前端常显示空白或 NaN。这不是数据问题,是 SQL 没兜底。
不能依赖应用层补零——窗口函数输出后就定型了。必须在 SQL 层用 CASE 显式拦截。尤其要注意:有些数据库(如 Presto)中 0/0 直接报错,而 PostgreSQL 返回 NULL,行为不一致。
- 安全写法:用
NULLIF(previous, 0)把分母 0 转成 NULL,再配合COALESCE(..., 0)统一返回 0 或其他标记值(如 -999) - 别用
WHERE previous != 0过滤——这会丢掉整个周期记录,导致时间轴断裂 - 如果业务允许,增长率可定义为
SAFE_DIVIDE(current - previous, previous)(BigQuery)或自定义函数,但标准 SQL 还是靠CASE
性能突然暴跌:窗口函数没加索引或过滤太宽
带 LAG() 的查询变慢,90% 是因为没限制时间范围。比如查最近 3 个月同比,却让窗口在全表上排序,数据库得把几百万行全扫一遍再排序。
窗口函数本身不走索引,但 WHERE 子句能提前剪枝。必须把时间过滤写在最外层子查询之前,而不是塞进窗口的 OVER() 里——后者无效。
- 正确姿势:先
WHERE order_date >= '2024-01-01',再GROUP BY,最后套窗口;不要等窗口输出完再WHERE ym >= 202401 - 给
order_date建索引有用,但对YEAR(order_date), MONTH(order_date)组合字段,建议建函数索引(MySQL 8.0+ 支持CREATE INDEX idx_ym ON t ((YEAR(order_date)*100+MONTH(order_date)))) - 分区表场景下,确保
WHERE条件能命中具体分区(如ds = '202403'),否则窗口仍会扫描所有分区
时间维度对齐比函数调用难得多——少一个 YEARWEEK 参数,或多一次 CAST,结果就偏一个月。实际跑之前,先用 SELECT ym, sum_sales, LAG(sum_sales) OVER (ORDER BY ym) 看三列是否严格对齐,比直接套公式靠谱。










