【SQL】SQL中同环比的多种计算方式
- 一、引言
- 二、同环比的核心概念
- 三、实现与案例
-
- [方法一:LEAD / LAG 窗口函数(最常用)](#方法一:LEAD / LAG 窗口函数(最常用))
-
- [1. 环比(按月)](#1. 环比(按月))
- [2. 同比(按年)](#2. 同比(按年))
- [方法二:自关联(Self JOIN)](#方法二:自关联(Self JOIN))
-
- [1. 环比自关联](#1. 环比自关联)
- [2. 同比自关联](#2. 同比自关联)
- [方法三:日期偏移法(DATE_SUB / DATE_ADD)](#方法三:日期偏移法(DATE_SUB / DATE_ADD))
-
- [1. 环比(DATE_SUB)](#1. 环比(DATE_SUB))
- [2. 同比(DATE_SUB 减1年)](#2. 同比(DATE_SUB 减1年))
- 四、总结对比
一、引言
数据分析里绕不开的一个话题------同环比计算
汇报的时候,管理者经常会问:"这个月比上个月增长了多少?"、"今年相比去年同期表现如何?" 这些问题背后的答案,都离不开同环比。
- 同比:和去年同一时期比
- 环比:和上一个周期比。
今天讲讲SQL的几种主流写法。
- SQL专题往期内容:
二、同环比的核心概念
基本概念梳理:
| 概念 | 说明 | 举例 |
|---|---|---|
| 同比(YoY) | 和去年同一时期比 | 今年4月 vs 去年4月 |
| 环比(MoM) | 和上一个周期比 | 今年4月 vs 今年3月 |
| 时间偏移函数 | 用 OFFSET / LAG 等取历史数据 | 数据分析必备 |
| 窗口函数 | 在分组内做聚合与偏移 | 复杂场景下更灵活 |
常见踩坑避雷点:
- 月份/日期不连续时,偏移后数据对不上
- 年度同期计算要注意闰年和月份天数差异
- 聚合粒度不同会导致结果偏差
三、实现与案例
下面以一张销售数据表 sales 为例,演示几种主流写法:
sql
-- 先建表
CREATE TABLE sales (
mt DATE, -- 销售月
amount DECIMAL(10,2) -- 销售额
);
测试数据如下:
| mt | amount |
|---|---|
| 2024-01-01 | 1000 |
| 2024-02-01 | 1200 |
| 2024-03-01 | 1100 |
| 2024-04-01 | 1500 |
| 2025-01-01 | 1300 |
| 2025-02-01 | 1400 |
| 2025-03-01 | 1350 |
方法一:LEAD / LAG 窗口函数(最常用)
原理: 使用 LAG(col, n)/LEAD(col,n) 往前/往后取 n 行的数据,直接在同一行里完成对比。这个函数也是面试中,经常会考的,一定要熟练应用。
优点:
- ✅ 一行出结果,逻辑清晰
- ✅ 性能好,窗口函数内部优化强
- ✅ 兼容 MySQL 8.0+ 和 Hive
缺点:
- ❌ 需要数据按时间连续,否则会跳空
- ❌ 不支持跨年/跨月批量偏移
1. 环比(按月)
sql
-- 环比:和上月比
SELECT
mt,
amount,
LAG(amount, 1) OVER (ORDER BY mt) AS last_month_amount, -- 上月销售额
amount - LAG(amount, 1) OVER (ORDER BY mt) AS moom_diff, -- 环比增长额
ROUND((amount - LAG(amount, 1) OVER (ORDER BY mt)) /
LAG(amount, 1) OVER (ORDER BY mt) * 100, 2) AS moom_rate -- 环比增长率%
FROM sales
ORDER BY mt;
结果:
| mt | amount | last_month_amount | moom_diff | moom_rate |
|---|---|---|---|---|
| 2024-02-01 | 1200 | 1000 | 200 | 20.00 |
| 2024-03-01 | 1100 | 1200 | -100 | -8.33 |
| ... | ... | ... | ... | ... |
2. 同比(按年)
sql
-- 同比:去年同一时期
SELECT
mt,
amount,
LAG(amount, 12) OVER (ORDER BY mt) AS last_year_amount, -- 去年同月
ROUND((amount - LAG(amount, 12) OVER (ORDER BY mt)) /
LAG(amount, 12) OVER (ORDER BY mt) * 100, 2) AS yoy_rate -- 同比增长率%
FROM sales
ORDER BY mt;
核心思路: 数据是月粒度,偏移 12 行就是去年同月。
方法二:自关联(Self JOIN)
原理: 把表和自身作JOIN,按时间条件关联历史数据。
优点:
- ✅ 适合复杂 JOIN 场景(多表联动)
缺点:
- ❌ 数据量翻倍,碰上大数据量长周期的,效率非常低
- ❌ 需要注意 NULL 情况(首月/首年无历史数据)
- ❌ 连续月份数据不能断层,否则漏关联
1. 环比自关联
sql
-- 环比自关联
SELECT
a.mt,
a.amount AS cur_amount,
b.amount AS last_month_amount,
a.amount - b.amount AS moom_diff,
ROUND((a.amount - b.amount) / b.amount * 100, 2) AS moom_rate
FROM sales a
LEFT JOIN sales b
ON DATE_SUB(a.mt, INTERVAL 1 MONTH) = b.mt -- 上月 = 本月减1个月
ORDER BY a.mt;
2. 同比自关联
sql
-- 同比自关联
SELECT
a.mt,
a.amount AS cur_amount,
b.amount AS last_year_amount,
ROUND((a.amount - b.amount) / b.amount * 100, 2) AS yoy_rate
FROM sales a
LEFT JOIN sales b
ON DATE_SUB(a.mt, INTERVAL 1 YEAR) = b.mt -- 去年同一月
ORDER BY a.mt;
方法三:日期偏移法(DATE_SUB / DATE_ADD)
原理: 直接构造目标日期,然后去原表查对应数据。适合临时查询和调试,原理其实和法2差不多
优点:
- ✅ 代码最简单,容易读懂
- ✅ 适合临时查询、一次性脚本
缺点:
- ❌ 大数据量下 JOIN 性能差
- ❌ 无索引时全表扫描,速度慢
- ❌ 日期不连续时结果会丢行
1. 环比(DATE_SUB)
sql
-- 环比:查上月
SELECT
a.mt,
a.amount,
b.amount AS last_month_amount,
ROUND((a.amount - b.amount) / b.amount * 100, 2) AS moom_rate
FROM sales a
LEFT JOIN sales b
ON b.mt = DATE_SUB(a.mt, INTERVAL 1 MONTH)
ORDER BY a.mt;
2. 同比(DATE_SUB 减1年)
sql
-- 同比:查去年同月
SELECT
a.mt,
a.amount,
b.amount AS last_year_amount,
ROUND((a.amount - b.amount) / b.amount * 100, 2) AS yoy_rate
FROM sales a
LEFT JOIN sales b
ON b.mt = DATE_SUB(a.mt, INTERVAL 1 YEAR)
ORDER BY a.mt;
四、总结对比
| 方法 | 原理 | 适用场景 | 难度 | 性能 |
|---|---|---|---|---|
| LEAD/LAG 窗口函数 | 窗口内偏移取历史行 | 月/周粒度数据,需连续排行 | ★★☆☆☆ | ⭐⭐⭐⭐ |
| 自关联 | 表和自身 JOIN 按时间条件 | 复杂多表联动/非连续时间 | ★★★☆☆ | ⭐⭐ |
| DATE_SUB 偏移 | 构造历史日期再查表 | 临时查询/数据不连续 | ★★☆☆☆ | ⭐⭐ |
推荐:
- 数据整齐、连续 → 用 LEAD/LAG 窗口函数,性能最好
- 复杂场景多表联动 → 用 自关联
- 临时调数据/老版本 MySQL → 用 DATE_SUB
划重点:窗口函数是首选方案,能解决大多数的同环比问题。