环比增长率需确保“上一期”是时间紧邻且真实存在的记录,不能仅依赖lag()取前一行,必须结合日期对齐、分组排序、空值与除零处理,并根据数据库特性选择add_months或date_sub构造严格周期。

什么是环比增长率,为什么不能直接用LAG()就完事?
环比增长率 = (当前期值 - 上一期值) / 上一期值 × 100%,关键在于“上一期”必须是时间上紧邻的、且该分组内真实存在的记录,而不是简单按时间排序后取前一行。常见错误是没考虑分组内日期不连续(比如缺某月数据)、或没处理除零(上一期值为0)、或没过滤掉首行导致 NULL 参与计算。
-
LAG()确实是核心函数,但它只按排序取上一行,不判断时间间隔是否为1个周期 - 必须显式定义“上一期”的时间基准(如按月环比就要用
DATE_SUB(month, 1)或ADD_MONTHS(-1)) - 分组字段(如
product_id)和时间字段(如report_month)必须同时参与ORDER BY和PARTITION BY - 计算前建议先用
WHERE report_month >= '2023-01-01'排除无效时间,避免早期 NULL 拉低整体结果
MySQL 8.0+ 实现月度分组环比:注意日期对齐和空值截断
MySQL 不支持 ADD_MONTHS(),得用 DATE_SUB(report_month, INTERVAL 1 MONTH) 构造上月日期,再用 LEFT JOIN 或窗口函数关联。更稳妥的做法是用 LAG() + 条件过滤:
SELECT
product_id,
report_month,
sales_amt,
LAG(sales_amt) OVER (PARTITION BY product_id ORDER BY report_month) AS prev_month_sales,
CASE
WHEN LAG(sales_amt) OVER (PARTITION BY product_id ORDER BY report_month) = 0
THEN NULL
ELSE (sales_amt - LAG(sales_amt) OVER (PARTITION BY product_id ORDER BY report_month))
/ LAG(sales_amt) OVER (PARTITION BY product_id ORDER BY report_month) * 100
END AS mom_rate
FROM sales_table
WHERE report_month >= '2023-02-01'; -- 至少从第二个月开始,避免首月计算出 NULL
-
WHERE条件必须放在外层或 CTE 中,不能在窗口函数里过滤,否则LAG()会跳过被过滤的行,导致“上一期”错位 -
mom_rate列用CASE显式处理除零,比直接写IFNULL(..., 0)更安全 - 如果原始表中
report_month是字符串(如'202301'),先转成STR_TO_DATE(report_month, '%Y%m')再排序,否则字典序会导致 '202312' 排在 '202401' 前
PostgreSQL / Oracle 中用 ADD_MONTHS() 做严格周期匹配
当业务要求“必须是自然月且不能跨月补数”时,LAG() 不够严谨。例如某产品 2023-03 销售额为 100,但 2023-02 数据缺失,LAG() 会取 2023-01 的值算环比,逻辑错误。此时应先生成完整的时间维度,再 LEFT JOIN 补空:
WITH month_series AS (
SELECT generate_series('2023-01-01'::date, '2023-12-01'::date, '1 month')::date AS month_end
),
full_grid AS (
SELECT p.product_id, m.month_end
FROM (SELECT DISTINCT product_id FROM sales_table) p
CROSS JOIN month_series m
)
SELECT
g.product_id,
g.month_end,
COALESCE(s.sales_amt, 0) AS sales_amt,
ROUND(
(COALESCE(s.sales_amt, 0) - COALESCE(s_prev.sales_amt, 0)) * 100.0 / NULLIF(s_prev.sales_amt, 0), 2
) AS mom_rate
FROM full_grid g
LEFT JOIN sales_table s
ON g.product_id = s.product_id AND g.month_end = s.report_month
LEFT JOIN sales_table s_prev
ON g.product_id = s_prev.product_id
AND s_prev.report_month = (g.month_end - INTERVAL '1 month');
-
NULLIF(s_prev.sales_amt, 0)比CASE WHEN s_prev.sales_amt = 0 THEN NULL更简洁,且避免除零报错 -
generate_series()在 PostgreSQL 中可用,Oracle 需用CONNECT BY或递归 CTE 替代 - 这种方式代价略高,但能确保“无数据即为 0”,环比结果可解释性强
别忽略浮点精度和业务口径差异
数据库算出来的 mom_rate 是数学值,但业务常要求“四舍五入到小数点后1位”或“负增长统一标为 -X.X%”。这些不是 SQL 能自动识别的:
-
ROUND(x, 2)在不同数据库中对 .5 的处理可能不同(如 MySQL 向偶数舍入,PostgreSQL 默认四舍五入) - 若上游系统把
0.00499当作 0.499%,而报表要求显示 “0.5%”,就得用ROUND(x <em> 100, 1)</em>而不是ROUND(x, 3) 100 - 最容易被跳过的点:有些业务定义“环比为负且绝对值
真正麻烦的从来不是写那几行 LAG(),而是确认“上一期”到底指什么——是数据库里有记录的上一行,还是日历上该有的上一周期。选错就全错。











