LAG()算环比最稳,因其天然适配“当前行与前一行对比”逻辑,比自关联更简洁可控;但必须配合唯一时间序的ORDER BY,补全缺失日期,并用NULLIF处理除零。

SQL中直接用LAG()和LEAD()算环比最稳
环比(比上期)本质是当前行与前一行的数值对比,LAG()天然适配这个逻辑,比自关联或子查询更简洁、可读性强、执行计划也更可控。关键不是“能不能”,而是“怎么写不出错”:
-
LAG()必须配合ORDER BY使用,且排序字段要能唯一确定时间序列顺序(比如stat_date,不能只靠month这种可能重复的字段) - 如果数据存在缺失月份(如2024-02没记录),
LAG()会跳过空缺,直接取上一个有值的记录——这会导致错误的环比(比如3月比1月),需先用GENERATE_SERIES(PostgreSQL)或日期维表补全 - 除零要显式处理:
NULLIF(daily_amount, 0)比CASE WHEN daily_amount = 0 THEN NULL ELSE ... END更短且安全
示例(PostgreSQL):
SELECT
stat_date,
daily_amount,
ROUND(
(daily_amount - LAG(daily_amount) OVER (ORDER BY stat_date)) * 100.0 /
NULLIF(LAG(daily_amount) OVER (ORDER BY stat_date), 0), 2
) AS mom_pct
FROM sales_daily;同比(比去年同期)必须用DATE_TRUNC()或日期运算对齐周期
同比不是简单LAG(..., 12)——那只是往前数12行,不保证是“去年同月同日”。真正靠谱的方式是构造去年同期时间点,再用JOIN或LEFT JOIN LATERAL匹配:
- PostgreSQL/Redshift:用
stat_date - INTERVAL '1 year'生成last_year_date,再JOIN原表 - MySQL 8.0+:用
DATE_SUB(stat_date, INTERVAL 1 YEAR),但要注意闰年2月29日会变成NULL,得加COALESCE(DATE_SUB(...), ...)兜底 - BigQuery:优先用
DATE_SUB(stat_date, INTERVAL 1 YEAR),它自动处理闰年边界
避免用LAG(col, 365)——天数不固定(闰年)、跨月逻辑错乱(1月1日的“去年”是前一年1月1日,不是365行前)。
聚合后计算增幅?先GROUP BY再套窗口函数,别反了
常见错误:在SUM(amount)之前就用LAG(),结果是对明细行做窗口,再求和,完全偏离业务含义。正确顺序是:
- 第一步:按时间粒度(如
YEAR_MONTH)分组聚合出指标值 - 第二步:对外层结果集再开窗口,用
LAG()取上期聚合值
错误写法(对明细行窗口,再求和):
SELECT SUM(amount), LAG(SUM(amount)) OVER (...) -- ❌
正确写法(先聚合,再窗口):
WITH monthly AS (
SELECT
DATE_TRUNC('month', order_time) AS ym,
SUM(amount) AS amt
FROM orders
GROUP BY 1
)
SELECT
ym,
amt,
ROUND((amt - LAG(amt) OVER (ORDER BY ym)) * 100.0 / NULLIF(LAG(amt) OVER (ORDER BY ym), 0), 2) AS mom
FROM monthly;不同数据库对NULL和除零的容忍度差异很大
同一个ROUND((cur - prev)/prev, 2)表达式,在不同引擎里行为可能完全不同:
- PostgreSQL:
10 / 0直接报错,必须用NULLIF(prev, 0) - MySQL:
10 / 0返回NULL(但开启sql_mode=STRICT_TRANS_TABLES时也会报错) - ClickHouse:
divide(a, b)函数可设default_value,比原生/更可控 - Spark SQL:
NULL参与运算结果为NULL,但0/0返回NULL而非报错
最保险的做法是统一用NULLIF()包裹分母,并在最外层用COALESCE(..., 0)或ROUND(..., 2)前加判断——别依赖引擎默认行为。
时间对齐、NULL 处理、聚合时机,这三个地方出错,环比同比数字就不可信。其他都是语法细节。

















