同比是与去年同期比较(如2024年3月 vs 2023年3月),环比是与上一期比较(如2024年3月 vs 2024年2月);仅用GROUP BY无法跨行获取历史值,必须用LAG()等窗口函数按时间顺序取值,并确保数据连续、排序严格、分组明确。

什么是同比和环比,为什么不能只用GROUP BY
同比是和去年同期比(比如2024年3月 vs 2023年3月),环比是和上一期比(比如2024年3月 vs 2024年2月)。单纯用 GROUP BY 汇总后,月份之间是割裂的——你拿不到“上个月的值”或“去年同月的值”,必须依赖窗口函数在有序行集内跨行取值。
LAG() 向前取(上期),LEAD() 向后取(下期),它们天然适合时间序列对比。但要注意:必须先按时间严格排序,且分组边界要清晰(比如按产品线、地区分组时,LAG() 不能跨组取值)。
实操建议:
- 时间字段必须可排序、无重复(否则需加
ROW_NUMBER()辅助去重) -
LAG(value, 1)表示取前1行,LAG(value, 12)才能取去年同月(前提是数据按月连续、无缺失) - 若原始数据是日粒度,计算同比/环比前先用
DATE_TRUNC('month', date)或TO_CHAR(date, 'YYYY-MM')归一化到月
用LAG实现环比:取上月值并计算增长率
核心是用 LAG() 在每个分组内按时间顺序取前一行的指标值。例如统计各产品线每月销售额的环比变化:
SELECT
product_line,
month,
sales,
LAG(sales, 1) OVER (PARTITION BY product_line ORDER BY month) AS prev_month_sales,
ROUND(
(sales - LAG(sales, 1) OVER (PARTITION BY product_line ORDER BY month)) * 100.0 /
NULLIF(LAG(sales, 1) OVER (PARTITION BY product_line ORDER BY month), 0),
2
) AS mom_pct
FROM monthly_sales;
关键点:
-
PARTITION BY product_line确保不同产品线不混算;漏掉它会导致所有产品线被当成一个序列 -
NULLIF(..., 0)防止除零错误——首月没有“上月”,LAG()返回 NULL,但显式处理更安全 - 结果中首月的
mom_pct是 NULL,这是正常行为,不是 bug
用LAG(…, 12)实现同比:小心数据断层和月份对齐
同比需要跳过12行,但前提是数据严格按月连续、无缺失。如果某产品线在2023年5月没数据,那么2024年5月调用 LAG(sales, 12) 取到的可能是2023年4月的值(取决于实际行序)。
更稳妥的做法是用时间偏移匹配:
- PostgreSQL / BigQuery:用
LAG(sales) OVER (PARTITION BY product_line ORDER BY month)+month = LAG(month, 12) + INTERVAL '12 months'做二次校验 - MySQL 8.0+:不支持 INTERVAL 在窗口帧里直接运算,建议先生成完整月维度表(LEFT JOIN 补全空月),再用
LAG(sales, 12) - 常见错误现象:
yoy_pct数值突兀跳变,大概率是某月数据缺失导致错位
LEAD能替代LAG做同比吗?适用场景有限
LEAD() 是向后取值,比如你想看“当前月相比下一年同期是涨是跌”,那可以用 LEAD(sales, 12)。但实际业务中几乎没人这么问——同比都是“现在 vs 过去”,所以 LEAD() 在同比场景基本用不上。
它真正有用的地方是预测类逻辑:
- 判断趋势是否持续:“本月涨,且下月还涨?” →
sales > LAG(sales) AND sales - 识别单月峰值:“本月比上月高、比下月也高” →
sales > LAG(sales) AND sales > LEAD(sales) - 注意:
LEAD()在最后一行返回 NULL,和LAG()在首行返回 NULL 对称
实际写同比环比时,99% 的情况只需 LAG()。把 LEAD() 当成备用工具就行,别为了用而用。
数据连续性永远比函数炫技重要。一旦发现同比结果异常,第一反应不该是查语法,而是检查源数据里有没有漏掉的月份。











