【SQL】SQL同环比计算的多种实现方式
【SQL】SQL中同环比的多种计算方式
- 一、引言
- 二、同环比的核心概念
- 三、实现与案例
- 方法一:LEAD / LAG 窗口函数(最常用)
- 1. 环比(按月)
- 2. 同比(按年)
- 方法二:自关联(Self JOIN)
- 1. 环比自关联
- 2. 同比自关联
- 方法三:日期偏移法(DATE_SUB / DATE_ADD)
- 1. 环比(DATE_SUB)
- 2. 同比(DATE_SUB 减1年)
- 四、总结对比
一、引言
数据分析里绕不开的一个话题——同环比计算
汇报的时候,管理者经常会问:“这个月比上个月增长了多少?”、“今年相比去年同期表现如何?” 这些问题背后的答案,都离不开同环比。
- 同比:和去年同一时期比
- 环比:和上一个周期比。
今天讲讲SQL的几种主流写法。
- SQL专题往期内容:
- 【SQL】基于多源SQL 去重方法对比 – 精华版
- 【SQL】常见SQL 行列转换的方法汇总 - 精华版
- 【SQL】MySql常见的性能优化方式
- 【SQL】SQL同环比计算的多种实现方式
- 【SQL】COUNT… FILTER 的适用场景
- 【SQL】SQL的日期与时间函数
- 【SQL】SQL-常见窗口函数有哪些-上篇
二、同环比的核心概念
基本概念梳理:
| 概念 | 说明 | 举例 |
|---|---|---|
| 同比(YoY) | 和去年同一时期比 | 今年4月 vs 去年4月 |
| 环比(MoM) | 和上一个周期比 | 今年4月 vs 今年3月 |
| 时间偏移函数 | 用 OFFSET / LAG 等取历史数据 | 数据分析必备 |
| 窗口函数 | 在分组内做聚合与偏移 | 复杂场景下更灵活 |
常见踩坑避雷点:
- 月份/日期不连续时,偏移后数据对不上
- 年度同期计算要注意闰年和月份天数差异
- 聚合粒度不同会导致结果偏差
三、实现与案例
下面以一张销售数据表sales为例,演示几种主流写法:
-- 先建表CREATETABLEsales(mtDATE,-- 销售月amountDECIMAL(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. 环比(按月)
-- 环比:和上月比SELECTmt,amount,LAG(amount,1)OVER(ORDERBYmt)ASlast_month_amount,-- 上月销售额amount-LAG(amount,1)OVER(ORDERBYmt)ASmoom_diff,-- 环比增长额ROUND((amount-LAG(amount,1)OVER(ORDERBYmt))/LAG(amount,1)OVER(ORDERBYmt)*100,2)ASmoom_rate-- 环比增长率%FROMsalesORDERBYmt;结果:
| 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. 同比(按年)
-- 同比:去年同一时期SELECTmt,amount,LAG(amount,12)OVER(ORDERBYmt)ASlast_year_amount,-- 去年同月ROUND((amount-LAG(amount,12)OVER(ORDERBYmt))/LAG(amount,12)OVER(ORDERBYmt)*100,2)ASyoy_rate-- 同比增长率%FROMsalesORDERBYmt;核心思路:数据是月粒度,偏移 12 行就是去年同月。
方法二:自关联(Self JOIN)
原理:把表和自身作JOIN,按时间条件关联历史数据。
优点:
- ✅ 适合复杂 JOIN 场景(多表联动)
缺点:
- ❌ 数据量翻倍,碰上大数据量长周期的,效率非常低
- ❌ 需要注意 NULL 情况(首月/首年无历史数据)
- ❌ 连续月份数据不能断层,否则漏关联
1. 环比自关联
-- 环比自关联SELECTa.mt,a.amountAScur_amount,b.amountASlast_month_amount,a.amount-b.amountASmoom_diff,ROUND((a.amount-b.amount)/b.amount*100,2)ASmoom_rateFROMsales aLEFTJOINsales bONDATE_SUB(a.mt,INTERVAL1MONTH)=b.mt-- 上月 = 本月减1个月ORDERBYa.mt;2. 同比自关联
-- 同比自关联SELECTa.mt,a.amountAScur_amount,b.amountASlast_year_amount,ROUND((a.amount-b.amount)/b.amount*100,2)ASyoy_rateFROMsales aLEFTJOINsales bONDATE_SUB(a.mt,INTERVAL1YEAR)=b.mt-- 去年同一月ORDERBYa.mt;方法三:日期偏移法(DATE_SUB / DATE_ADD)
原理:直接构造目标日期,然后去原表查对应数据。适合临时查询和调试,原理其实和法2差不多
优点:
- ✅ 代码最简单,容易读懂
- ✅ 适合临时查询、一次性脚本
缺点:
- ❌ 大数据量下 JOIN 性能差
- ❌ 无索引时全表扫描,速度慢
- ❌ 日期不连续时结果会丢行
1. 环比(DATE_SUB)
-- 环比:查上月SELECTa.mt,a.amount,b.amountASlast_month_amount,ROUND((a.amount-b.amount)/b.amount*100,2)ASmoom_rateFROMsales aLEFTJOINsales bONb.mt=DATE_SUB(a.mt,INTERVAL1MONTH)ORDERBYa.mt;2. 同比(DATE_SUB 减1年)
-- 同比:查去年同月SELECTa.mt,a.amount,b.amountASlast_year_amount,ROUND((a.amount-b.amount)/b.amount*100,2)ASyoy_rateFROMsales aLEFTJOINsales bONb.mt=DATE_SUB(a.mt,INTERVAL1YEAR)ORDERBYa.mt;四、总结对比
| 方法 | 原理 | 适用场景 | 难度 | 性能 |
|---|---|---|---|---|
| LEAD/LAG 窗口函数 | 窗口内偏移取历史行 | 月/周粒度数据,需连续排行 | ★★☆☆☆ | ⭐⭐⭐⭐ |
| 自关联 | 表和自身 JOIN 按时间条件 | 复杂多表联动/非连续时间 | ★★★☆☆ | ⭐⭐ |
| DATE_SUB 偏移 | 构造历史日期再查表 | 临时查询/数据不连续 | ★★☆☆☆ | ⭐⭐ |
推荐:
- 数据整齐、连续 → 用LEAD/LAG 窗口函数,性能最好
- 复杂场景多表联动 → 用自关联
- 临时调数据/老版本 MySQL → 用DATE_SUB
划重点:窗口函数是首选方案,能解决大多数的同环比问题。
