同比波动是当前周期值与去年同期值的差值或比率,如2024年3月 vs 2023年3月;GROUP BY无法自动关联跨年同期数据,因SQL无内置时间偏移逻辑,需用DATE_SUB等函数显式构造去年同期键并连接或分区计算。

什么是同比波动,为什么不能直接用 GROUP BY 算?
同比波动本质是「当前周期值 vs 同期历史值」的差值或比率,比如 2024年3月销售额 vs 2023年3月销售额。单纯用 GROUP BY 只能聚合当前数据,无法自动关联去年同月——SQL 没有内置“往前推一年”这种时间偏移逻辑,必须显式构造时间匹配关系。
常见错误是写成:SELECT month, SUM(sales), SUM(sales) - LAG(SUM(sales)) OVER (ORDER BY month),这算的是环比(相邻月份),不是同比(同月跨年)。
用 DATE_SUB() 或 ADD_MONTHS() 构造同比时间键
核心思路:给每条记录生成一个“去年同期对应时间”,再用这个字段做连接或窗口分区。不同数据库函数名不同,但逻辑一致:
- MySQL / Spark SQL:
DATE_SUB(date_col, INTERVAL 1 YEAR) - PostgreSQL:
(date_col - INTERVAL '1 year')::DATE - Oracle / Hive:
ADD_MONTHS(date_col, -12) - 注意:如果原始字段是
DATETIME或带时分秒,务必先CAST或DATE()截断到日级,否则跨年匹配可能因时间精度错位
示例(MySQL):
SELECT
DATE_FORMAT(order_date, '%Y-%m') AS ym,
SUM(amount) AS cur_amt,
prev.prev_amt,
ROUND((SUM(amount) - prev.prev_amt) / NULLIF(prev.prev_amt, 0), 4) AS yoy_rate
FROM orders o
LEFT JOIN (
SELECT
DATE_FORMAT(DATE_SUB(order_date, INTERVAL 1 YEAR), '%Y-%m') AS ym,
SUM(amount) AS prev_amt
FROM orders
WHERE order_date >= '2023-01-01' AND order_date < '2024-01-01'
GROUP BY DATE_FORMAT(DATE_SUB(order_date, INTERVAL 1 YEAR), '%Y-%m')
) prev ON DATE_FORMAT(o.order_date, '%Y-%m') = prev.ym
WHERE o.order_date >= '2024-01-01' AND o.order_date < '2025-01-01'
GROUP BY ym, prev.prev_amt;
用窗口函数替代自连接,避免重复扫描
当数据量大、维度多(如按区域+品类分组),自连接容易拖慢。可用窗口函数 + 时间偏移预计算,前提是日期字段连续且无缺失:
- 先用
DATE_FORMAT(order_date, '%Y-%m')生成分组键ym - 再用
SUM(amount) OVER (PARTITION BY ym ORDER BY ym ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)不够——窗口无法跨年跳转 - 正确做法:用
LAG(SUM(amount), 12) OVER (ORDER BY ym),但要求ym是严格连续的字符串(如 '2023-01', '2023-02', ..., '2024-01'),且中间不能缺月;否则第13行的LAG(..., 12)会取到 2023-01 之前的值,而非 2023-01 - 更稳的方式:把年份和月份拆成两列
year,month,然后LAG(sum_amt) OVER (PARTITION BY month ORDER BY year),这样每月独立拉取前一年同月
NULL 和除零问题必须显式处理
同比计算最常崩在分母为 0 或去年无数据时:NULL / 100 得 NULL,100 / 0 直接报错。别依赖业务层兜底:
- 分母用
NULLIF(prev_amt, 0)替代直接除,返回NULL而非报错 - 结果用
COALESCE(yoy_rate, 0)或CASE WHEN prev_amt = 0 THEN NULL ELSE ... END明确语义 - 如果去年该分组完全没数据(如新上线区域),
LEFT JOIN后prev_amt为NULL,此时同比应为NULL或标记为「新组」,而不是当成 0 计算
实际跑之前,先用 SELECT COUNT(*) FROM orders WHERE order_date >= '2023-01-01' AND order_date 确认历史数据存在性——很多“同比为 NULL”只是去年压根没数据,不是 SQL 写错了。

















