分组内环比增长率需在每组内按时间顺序计算相邻两期变化率,必须用lag()配合partition by和order by,否则跨组污染;首行及除零需null防护,推荐cte先取上期值再计算。

什么是分组内环比增长率,为什么不能直接用LAG()就完事
环比增长率 = (当前值 - 上期值) / 上期值,核心是“同一分组内按时间/顺序排列的相邻两行做差”。很多人一上来就写 LAG(value),结果发现所有分组的边界处算出错误值——比如A组最后一条和B组第一条被连起来算。这是因为没加 PARTITION BY,LAG() 默认在整个结果集上滑动,不是按业务分组。
关键点在于:必须显式声明分组维度和排序逻辑,否则跨组污染不可避免。
实操建议:
-
LAG()必须配合PARTITION BY group_col ORDER BY time_col使用,缺一不可 - 如果时间字段有重复(如按天聚合但多条记录同一天),需额外加唯一排序键(如
id或ROW_NUMBER())避免窗口错位 - 首行(每组第一条)的
LAG()返回NULL,除法前要处理,否则整列变NULL
怎么写出安全、可读的环比SQL(含NULL防护)
直接写 (value - LAG(value) OVER (...)) / LAG(value) OVER (...) 看似简洁,但重复调用 LAG() 效率低,且两次调用结果未必严格一致(尤其在复杂排序下)。更稳妥的做法是用子查询或 CTE 先拉出上期值,再统一计算。
SELECT
region,
dt,
value,
CASE
WHEN prev_value IS NOT NULL AND prev_value != 0
THEN ROUND((value - prev_value)::DECIMAL / prev_value, 4)
ELSE NULL
END AS mom_rate
FROM (
SELECT
region,
dt,
value,
LAG(value) OVER (PARTITION BY region ORDER BY dt) AS prev_value
FROM sales_daily
) t;
注意几个细节:
- 显式转
::DECIMAL避免整数除法截断(PostgreSQL);MySQL 用CAST(value AS DECIMAL) -
prev_value != 0判断必须存在,否则除零报错(如某地区某天销量为 0,下一天恢复,上期值=0) -
ROUND(..., 4)控制小数位,避免浮点误差干扰业务判断
不同数据库对LAG()的兼容性差异有哪些
LAG() 在主流数据库都支持,但默认行为和参数细节不同:
常见差异点:
- PostgreSQL / Redshift / BigQuery:支持
LAG(value, offset, default)三参数,default可设为 0 或 1,避免 NULL 传播 - MySQL 8.0+:语法一致,但
ORDER BY必须明确,不支持无序窗口 - SQL Server:同样支持三参数,但
default值类型必须与value严格一致,否则报类型转换错 - Oracle:支持,但旧版本(11g)不支持
OFFSET参数,只能用LAG(value, 1, 0)这种固定偏移
建议统一写法:
- 始终显式写
LAG(value, 1, NULL),不依赖默认值,提高可读性 - 生产环境避免用
0做默认值代替NULL,否则会把真实缺失值和补零混淆
当数据存在空日期(如节假日无销售)时怎么处理
真实业务中,dt 往往不是连续的:周末、节假日没有记录,但环比要求“自然日”对比(比如周一 vs 周日),而不是“上一条记录”对比。这时 LAG() 按原始数据排序就失效了。
解决思路只有两个:
- 预填充缺失日期(用
GENERATE_SERIES或日期维表LEFT JOIN),把空值补为 0 或NULL,再跑LAG() - 改用自连接或 correlated subquery 找“最近的前一个有效日期”,性能差但语义准确,例如:
(SELECT value FROM sales_daily s2 WHERE s2.region = s1.region AND s2.dt
多数场景推荐第一种——虽然 ETL 成本略高,但窗口函数能保持向量化执行优势,且逻辑清晰可控。补空之后,LAG() 就又可靠了。
实际写的时候最容易漏掉的是分组边界检查和除零保护,这两处一出错,整批环比数据就不可信。别图省事跳过 CASE WHEN。










