在业务报表开发中,同比和环比是最基础也最常用的指标计算方式。同比通常指与上年同一时期相比,环比则是与相邻的上一个统计周期相比。过去很多团队习惯用自连接或者相关子查询来对齐时间,不仅SQL冗长,而且在缺失某些月份数据时结果容易出错。窗口函数LAG和LEAD的出现,让我们可以在不破坏原表行数的前提下,直接拿到当前行之前或之后的某行数值,从而用一行表达式完成对比。

一、LAG与LEAD函数基本用法
LAG和LEAD都属于偏移类的窗口函数。LAG用于获取当前行之前第N行的数据,LEAD用于获取当前行之后第N行的数据。它们的基本语法结构相同,都包含三个参数:第一个是取值的列,第二个是偏移量(默认是1),第三个是当偏移超出范围时返回的默认值(可省略,默认返回NULL)。
窗口函数的定义依赖于OVER子句,在OVER中通过PARTITION BY划分分组,通过ORDER BY确定偏移的顺序。例如,在按店铺分区、按月份排序的情况下,LAG(amount, 1)就是取同一个店铺上个月的数据。理解这一点非常关键,因为如果没有正确的ORDER BY,偏移方向就会混乱,计算结果毫无意义。
-- 基本语法示例 SELECT shop_id, stat_month, amount, LAG(amount, 1) OVER (PARTITION BY shop_id ORDER BY stat_month) AS prev_month_amount, LEAD(amount, 1) OVER (PARTITION BY shop_id ORDER BY stat_month) AS next_month_amount FROM sales_monthly;
二、准备示例数据
为了直观演示,我们先建立一张简单的月度销售表,并插入跨年的数据。表中包含店铺编号、统计月份和销售额三个字段。注意月份使用标准的YYYY-MM格式字符串,这样在按字符串排序时也能自然保持时间顺序,不需要额外转换成日期类型。
下面给出的数据里,每个店铺都有连续十二个月以上的记录,方便我们观察同比(间隔12个月)和环比(间隔1个月)的对应关系。实际业务中如果某些月份缺失,LAG和LEAD会如实返回NULL,我们后续会讲如何处理这种空值。
CREATE TABLE sales_monthly ( shop_id INT, stat_month VARCHAR(7), amount DECIMAL(12,2) ); INSERT INTO sales_monthly VALUES (1, '2022-01', 1000.00), (1, '2022-02', 1200.00), (1, '2023-01', 1500.00), (1, '2023-02', 1700.00), (2, '2022-01', 800.00), (2, '2022-02', 900.00), (2, '2023-01', 1100.00), (2, '2023-02', 1300.00);
三、使用LAG计算环比与同比
环比只需要取前一行数据,也就是偏移量为1的LAG值。我们用当前月销售额减去上月销售额,再除以上月销售额,就得到环比增长率。对于同比,偏移量应设为12,即取上一年同月的销售额进行计算。由于我们在OVER中按shop_id分区,所以不同店铺之间不会互相错位。
在写计算表达式时,建议对分母做NULL或者零值保护,否则当某个月没有上月数据时,除法会返回NULL,影响报表展示。可以用COALESCE把NULL转换成0,或者直接在外层用CASE WHEN过滤掉无法计算的首月记录。
SELECT
shop_id,
stat_month,
amount,
-- 环比:上月数据
LAG(amount, 1) OVER (PARTITION BY shop_id ORDER BY stat_month) AS last_month,
-- 同比:去年同月数据
LAG(amount, 12) OVER (PARTITION BY shop_id ORDER BY stat_month) AS last_year_same_month,
-- 环比增长率
CASE
WHEN LAG(amount, 1) OVER (PARTITION BY shop_id ORDER BY stat_month) IS NULL THEN NULL
ELSE (amount - LAG(amount, 1) OVER (PARTITION BY shop_id ORDER BY stat_month))
/ LAG(amount, 1) OVER (PARTITION BY shop_id ORDER BY stat_month)
END AS mom_rate,
-- 同比增长率
CASE
WHEN LAG(amount, 12) OVER (PARTITION BY shop_id ORDER BY stat_month) IS NULL THEN NULL
ELSE (amount - LAG(amount, 12) OVER (PARTITION BY shop_id ORDER BY stat_month))
/ LAG(amount, 12) OVER (PARTITION BY shop_id ORDER BY stat_month)
END AS yoy_rate
FROM sales_monthly;
四、用LEAD做反向校验与未来预估
LEAD函数和LAG方向相反,它取的是当前行之后的数据。在同比环比场景里,LEAD用得相对少,但可以用来做反向校验,比如从年末往前看,确认下一年同月是否已经入库;或者在做滚动预测时,拿当前行去对比下一周期的目标值。
下面示例展示了如何用LEAD取出下个月和明年同月的值,并计算其与当前月的差值。这种写法在制作双向对比报表时很方便,不需要再写第二个查询。
SELECT shop_id, stat_month, amount, LEAD(amount, 1) OVER (PARTITION BY shop_id ORDER BY stat_month) AS next_month, LEAD(amount, 12) OVER (PARTITION BY shop_id ORDER BY stat_month) AS next_year_same_month FROM sales_monthly;
五、空值处理与性能注意点
当偏移量超出分组边界,LAG和LEAD会返回NULL。在报表中,首月没有环比、前十二个月没有同比是正常现象。如果希望用0代替NULL参与后续运算,可以在函数里传第三个参数,例如LAG(amount, 1, 0)。但要注意,用0做分母会得到除零错误,所以更稳妥的做法是保留NULL并用CASE WHEN控制输出。
从性能角度看,LAG和LEAD只需对数据做一次分区排序,执行效率远高于自连接。但在数据量极大时,依然要确保PARTITION BY和ORDER BY的列有合适索引,或者统计信息准确,避免数据库在排序阶段产生大量临时文件。另外,如果业务里月份本身可能不连续,用固定偏移量12算同比会错位,此时应先生成连续月份维度表再做关联,而不是盲目依赖偏移量。
| 对比方式 | 偏移量 | 常用函数 | 首期数据表现 |
|---|---|---|---|
| 环比 | 1 | LAG | 首月为NULL |
| 同比 | 12 | LAG | 前12个月为NULL |
| 下期对比 | 1或12 | LEAD | 末期为NULL |
六、完整查询示例
最后给出一个可直接运行的完整查询,把环比和同比增长率以百分比形式展示出来,并过滤掉无法计算的首年记录。你可以把它包装成视图,供前端报表直接调用。
SELECT
shop_id,
stat_month,
amount,
ROUND(
(amount - LAG(amount, 1) OVER w) / NULLIF(LAG(amount, 1) OVER w, 0) * 100,
2
) AS mom_growth_percent,
ROUND(
(amount - LAG(amount, 12) OVER w) / NULLIF(LAG(amount, 12) OVER w, 0) * 100,
2
) AS yoy_growth_percent
FROM sales_monthly
WINDOW w AS (PARTITION BY shop_id ORDER BY stat_month);
通过上述写法,我们用标准的SQL窗口函数清晰实现了同比和环比的计算,既减少了表关联,也降低了脚本维护成本。在大部分支持窗口函数的数据库里,这套逻辑都可以无缝迁移。