环比(mom)和同比(yoy)是当前周期值与前一周期(上月/上年同月)值的相对变化率,非简单聚合,故不能直接用avg()/sum();需用lag()等窗口函数跨行取值,并严格排序、规整日期粒度、处理分母为零。

什么是环比和同比,为什么不能直接用 AVG() 或 SUM() 算?
环比(MoM)和同比(YoY)本质是「当前值与前一周期值比较」的相对变化,不是简单聚合。SQL 的标准聚合函数如 AVG()、SUM() 只能横向压缩行,无法跨行取“上个月”或“去年同月”的值——这需要窗口函数或自连接。
常见错误是试图用 GROUP BY month 后再套 LAG() 却忘了排序,导致错位;或用 JOIN 但没处理日期边界(比如 2023-01 对不上 2022-01,因闰年/月末差异)。
-
LAG()是最常用解法,但必须配合ORDER BY和确定的分组粒度(如按year_month排序) - 日期字段建议先规整为标准周期标识,例如用
DATE_FORMAT(order_date, '%Y-%m')(MySQL)或TO_CHAR(order_date, 'YYYY-MM')(PostgreSQL) - 避免直接用
order_date - INTERVAL '1 month'做 JOIN 条件——2023-03-31 减一个月是 2023-02-31(非法日期),多数数据库会转成 2023-03-03,造成错配
用 LAG() 计算环比增长率(MoM)
核心是:对每个周期,取出上一周期的聚合值,再计算差值比。注意分母为 0 要判空,否则出现 division by zero 错误。
SELECT
year_month,
revenue,
LAG(revenue) OVER (ORDER BY year_month) AS prev_revenue,
CASE
WHEN LAG(revenue) OVER (ORDER BY year_month) = 0 THEN NULL
ELSE ROUND((revenue - LAG(revenue) OVER (ORDER BY year_month)) / LAG(revenue) OVER (ORDER BY year_month) * 100, 2)
END AS mom_rate
FROM (
SELECT
DATE_FORMAT(order_date, '%Y-%m') AS year_month,
SUM(amount) AS revenue
FROM orders
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
) t;
- 必须确保子查询中已按
year_month聚合,否则LAG()作用在原始明细行上毫无意义 -
LAG(revenue) OVER (ORDER BY year_month)中的ORDER BY不能省略,且字段必须和分组一致 - MySQL 8.0+、PostgreSQL 8.4+、SQL Server 2012+ 支持
LAG();SQLite 3.25+ 也支持,但旧版不支持
用 LAG() 计算同比增长率(YoY)
同比的关键是把“去年同月”作为偏移量。用 LAG(revenue, 12) 表示跳过 12 行——但这只在数据严格按月连续、无缺失时才可靠。更稳妥的是用日期运算对齐周期:
SELECT
year_month,
revenue,
LAG(revenue, 12) OVER (ORDER BY year_month) AS yoy_revenue,
CASE
WHEN LAG(revenue, 12) OVER (ORDER BY year_month) = 0 THEN NULL
ELSE ROUND((revenue - LAG(revenue, 12) OVER (ORDER BY year_month)) / LAG(revenue, 12) OVER (ORDER BY year_month) * 100, 2)
END AS yoy_rate
FROM (
SELECT
DATE_FORMAT(order_date, '%Y-%m') AS year_month,
SUM(amount) AS revenue
FROM orders
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
) t;
-
LAG(revenue, 12)隐含假设:结果集已按自然月严格排序,且每月都有数据。若 2022-02 缺失,则 2023-02 的LAG(..., 12)会取到 2022-01,而非真正的去年同期 - 真正健壮的做法是生成完整时间维度表 LEFT JOIN,但成本高;日常报表中,先检查
COUNT(DISTINCT year_month)是否等于预期月份数,再决定能否用LAG(..., 12) - Oracle 用户注意:
LAG()第二参数是 offset,默认为 1,写成LAG(revenue, 12)即可,不要写LAG(revenue, 12, 0)(第三个参数是默认值,非必需)
遇到 NULL 或 division by zero 怎么办?
环比/同比首月、数据断层月、分母为零的月份必然产出 NULL 或报错。不能靠前端忽略,得在 SQL 层兜底。
- 用
CASE WHEN ... THEN ... ELSE NULL END显式处理分母为 0,比COALESCE(LAG(...), 0)更安全——后者会让 0/0 变成 0,业务上完全错误 - 如果需显示“-”或“—”这类标记,用
COALESCE(CAST(yoy_rate AS CHAR), '—'),但注意类型转换可能失败,优先用字符串拼接CONCAT(ROUND(...), '%') - 某些场景(如监管报表)要求“不可用数据填 0”,这时必须和业务方确认:0% 和 “无数据” 是完全不同含义,硬填 0 是埋雷
真正麻烦的不是写法,而是时间颗粒度对齐——销售数据按订单日,财务数据按入账日,库存数据按盘点日。三个来源的“2023-06”根本不是同一组天数。别急着写 LAG(),先拉齐口径。











