SQL 中如何计算每组同比增长率?

栗子
栗子
发布时间:2026-08-25 | 139人浏览过
原创
SQL 计算同比时最常见的三类错误排查卡片图
同比 SQL 的排错重点结果全为 NULL 或数值异常时,先排查时间粒度、分组键和除零处理。
LAG(value,12) 只有在月序列连续时才等价于同比的条件对比图
LAG 算同比的适用前提窗口函数能写得更短,但前提是时间序列连续且已经补齐。
按年月与分组键对齐后,通过 LEFT JOIN 关联去年同期并计算同比的流程图
同比计算的稳妥流程先聚合、再按去年同期关联,是多数 SQL 方言里更稳妥的同比写法。

前言

同比增长率最容易写错的地方,不是公式本身,而是把“去年同周期”误写成“上一行数据”。这篇文章会先拆清 `LAG()` 为什么常把同比算成环比,再给出兼容性更好的 `LEFT JOIN + 日期偏移` 写法,并补上判断 `LAG(value, 12)` 是否可靠的前提和排错思路。

同比增长率看起来只是一个简单公式,但真正容易出错的地方在于“同期”二字。只要时间序列不连续、分组键不完整,很多人顺手写出的 LAG() 就会把同比算成环比。下面按判断逻辑、通用写法和排错信号三部分展开,帮助你先确认口径,再决定该用自连接还是窗口函数。

什么是同比增长率,为什么不能直接用 LAG()

同比增长率的标准公式是:(本期值 - 同期值) / 同期值。这里的“同期值”必须是上一年同一时间周期的数据,比如 2024 年 3 月要对比 2023 年 3 月,而不是简单对比结果集中的上一行。

这也是很多 SQL 写法出错的根源。像 LAG(value, 1) 这样的写法,取到的是“前一行”,通常对应相邻月份或相邻记录,本质更接近环比,而不是同比。只要数据里有缺月、跳月,或者同一个分组的数据并不严格连续,结果就会被悄悄带偏,而且不一定会直接报错。

因此,算同比时要先把时间维度对齐,再去关联“当前周期”和“去年同周期”。常见做法有两类:一种是先聚合后自连接,兼容性最好;另一种是在时间序列补齐后再用 LAG(value, 12),但前提比较严格。

用 LEFT JOIN + 日期偏移计算同比

如果你希望写法稳、兼容性强,而且后续还要做索引优化或排查问题,优先考虑 LEFT JOIN。这种方法适用于 MySQL、PostgreSQL、SQL Server 等常见数据库,核心思路也比较直观:先得到“本期数据”,再去找“去年同月”的对应值。

实现思路

  • 先按 year、month,或者按 date_trunc('month', order_date) 把原始数据聚合成每组每月一条记录,形成当前周期表,例如 t_curr
  • 再准备去年同期表 t_prev,通过日期减去 1 年来建立对应关系。MySQL 常用 DATE_SUB(..., INTERVAL 1 YEAR),PostgreSQL 常用 - INTERVAL '1 year'
  • 使用 LEFT JOIN 把本期记录和去年同期记录关联起来,这样即便去年没有数据,本期数据也不会丢失
  • 最后计算增长率时,用 NULLIF 避免除零,用 COALESCE 控制空值结果是保留为 NULL 还是转成 0

PostgreSQL 示例

SELECT
t_curr.year_month,
t_curr.group_name,
ROUND(
COALESCE((t_curr.total - t_prev.total) * 1.0 / NULLIF(t_prev.total, 0), 0),
4
) AS yoy_rate
FROM monthly_sales t_curr
LEFT JOIN monthly_sales t_prev
ON t_curr.group_name = t_prev.group_name
AND t_prev.year_month = t_curr.year_month - INTERVAL '1 year';

这段 SQL 的好处在于,逻辑和业务定义是一一对应的:本期是谁、同期是谁、缺失时怎么处理,都能直接看出来。对资讯站读者来说,这种写法也更容易调试,因为一旦结果不对,可以先单独查 t_curr 和 t_prev 是否聚合正确,再检查关联条件是否完整。

另外,增长率缺失后到底显示 NULL 还是 0,不要只看技术实现,还要看业务口径。如果去年没有同期数据,把结果直接补成 0 虽然方便展示,但可能会掩盖“无可比基数”的事实,这一点在报表里要提前确认。

什么时候可以用窗口函数和 LAG(value, 12)

窗口函数并不是不能算同比,而是约束条件比很多人想象得更严格。像 BigQuery、Trino 以及部分新版 PostgreSQL,可以配合时间列排序来处理序列数据,但真正常见的写法还是 LAG(total, 12)。问题在于,12 表示前 12 行,不是天然代表前 12 个自然月。

只有在下面几个条件同时满足时,LAG(value, 12) 才近似等价于同比:

  • 每个分组下严格做到每月一条记录
  • 年月序列连续,中间没有缺失月份
  • 排序字段稳定,通常要明确按 year, month 排序
  • 原始数据中的空档已经补齐

如果其中任何一个条件不满足,LAG(..., 12) 都可能取错行。比如缺了 2024 年 2 月的数据,那么往前 12 行时,很可能拿到的不是 2023 年 3 月,而是别的月份,结果表面上能算出来,实际已经偏离同比定义。

必须先补全时间序列

因此,若你一定要用 LAG,先补齐所有年月组合再说。PostgreSQL 可以借助 GENERATE_SERIES,其他数据库也可以用 UNION ALL 等方式手工构造完整日历表,然后再和原始聚合结果做 LEFT JOIN。只有把缺失月份显式补出来,窗口函数按 12 行回看才有意义。

还有一个常见误区是 Oracle 中的 LAG(value, 12)。这里的 12 同样是行偏移,不是“12 个日历月”。如果数据表不是天然连续的月序列,就不能直接把它当同比使用。

分组后计算同比失败的三个典型信号

很多同比 SQL 不是写不出来,而是写完后结果全是 NULL、值明显不对,或者不同数据库表现不一致。遇到这种情况,通常先看下面三个位置。

1. 时间字段没有归一化

如果直接拿 order_date 去分组或排序,而不是先统一到月、季度这样的业务粒度,就容易出现错位。比如你写了按年、月提取字段,但底层数据仍是日级记录,那么同一个月的多行数据和跨年边界混在一起后,窗口函数就可能在错误的位置取值。

更稳妥的方式是先生成统一的时间键,例如月度报表就先落成 year_month,季度报表就先落成季度字段,再进入后续同比计算。

2. GROUP BY 漏掉业务分组键

这是报表类 SQL 中非常常见的问题。比如你想按 region 看同比,却只按年份或月份聚合,最后得到的就是所有地区混合后的总值,再去同比自然会失真。

判断方法很简单:凡是你希望在结果中独立展示的维度,通常都应该同时出现在聚合和关联条件里。对于示例中的 group_name,它既要参与当前期聚合,也要出现在与去年同期的连接条件中,否则不同组别之间会相互串值。

3. 没处理除零和空值

同比公式里分母是去年同期值,所以只要去年同期为 0 或者不存在,就会触发异常或返回空值。这里至少要明确两层处理:

  • 用 NULLIF(denominator, 0) 规避除零
  • 再根据报表需求决定是否用 COALESCE(..., 0) 把结果补成 0

不同数据库的表现也不一样。PostgreSQL 遇到除零通常直接报 division by zero;MySQL 可能返回 NULL,但不会明显提示,这就容易让人误判成“去年没数据”。因此,别只盯着最终结果,先把分母列单独查出来看一遍,往往更快。

落地时先确认口径,再决定写法

从实现角度看,最稳妥的路径依然是先统一时间粒度,再用 LEFT JOIN + 去年同期偏移 计算同比;只有在时间序列已经补齐、每组严格连续的前提下,LAG(value, 12) 才适合作为简化写法。

真正难的部分通常不在 SQL 语法,而在业务口径确认上。你需要核对的是:当前报表到底按自然月、财年还是业务自定义周期统计;去年同期是否存在有效可比数据;缺失值和零值在报表上分别该怎么展示。把这些条件先确认清楚,再回头选 SQL 写法,结果才不会看着正确、实际跑偏。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表