环比是与上一周期(如本月vs上月)比较,同比是与去年同期(如本月vs去年同月)比较;SQL中需用DATE_SUB配合LAST_DAY精准对齐时间点,通过LEFT JOIN主表当前周期数据确保NULL可显,避免索引失效与除零错误。

什么是环比和同比,SQL里怎么表达时间差
环比是和上一个周期比(比如本月 vs 上月),同比是和去年同一周期比(比如本月 vs 去年同月)。关键不在“比”,而在“怎么精准取到那个‘上期’或‘去年同期’的记录”。DATE_SUB 和 DATE_ADD 是最直接的工具,但必须配合 GROUP BY 和外层聚合对齐时间粒度——否则容易出现一对多或漏匹配。
常见错误现象:LEFT JOIN 用 YEAR(date) = YEAR(date)-1 导致全表扫描;或者用 WHERE date BETWEEN ... AND ... 错误过滤掉主表当期数据。
- 月份环比:用
DATE_SUB(t1.month_end, INTERVAL 1 MONTH)计算上月月末,再与子查询结果ON t2.month_end = DATE_SUB(t1.month_end, INTERVAL 1 MONTH) - 月份同比:用
DATE_SUB(t1.month_end, INTERVAL 1 YEAR),注意闰年 2 月 29 日可能无对应日,建议统一用月末函数LAST_DAY(DATE_SUB(t1.month_end, INTERVAL 1 YEAR)) - 避免在
JOIN条件里写YEAR(t1.date) = YEAR(t2.date) - 1 AND MONTH(t1.date) = MONTH(t2.date)—— 索引失效,性能极差
嵌套查询结构怎么搭才不丢数据
财务报表要求“当期有数、上期/同期没数也要显示为 NULL”,所以主表必须是当前周期的汇总结果,再用 LEFT JOIN 拉取对比期。不能把对比逻辑塞进 WHERE 或子查询的 SELECT 里——那样会过滤掉 NULL 行。
使用场景:按部门+科目汇总月度收入,同时展示各科目的环比增长率和同比增长率。
SELECT
cur.dept,
cur.subject,
cur.amount AS amount_cur,
prev.amount AS amount_prev,
ROUND((cur.amount - IFNULL(prev.amount, 0)) / NULLIF(prev.amount, 0), 4) AS mom_rate,
yoy.amount AS amount_yoy,
ROUND((cur.amount - IFNULL(yoy.amount, 0)) / NULLIF(yoy.amount, 0), 4) AS yoy_rate
FROM (
SELECT dept, subject, SUM(amount) AS amount, LAST_DAY(trade_date) AS month_end
FROM finance_log
WHERE trade_date >= '2024-01-01'
GROUP BY dept, subject, LAST_DAY(trade_date)
) cur
LEFT JOIN (
SELECT dept, subject, SUM(amount) AS amount, LAST_DAY(trade_date) AS month_end
FROM finance_log
GROUP BY dept, subject, LAST_DAY(trade_date)
) prev ON cur.dept = prev.dept
AND cur.subject = prev.subject
AND prev.month_end = DATE_SUB(cur.month_end, INTERVAL 1 MONTH)
LEFT JOIN (
SELECT dept, subject, SUM(amount) AS amount, LAST_DAY(trade_date) AS month_end
FROM finance_log
GROUP BY dept, subject, LAST_DAY(trade_date)
) yoy ON cur.dept = yoy.dept
AND cur.subject = yoy.subject
AND yoy.month_end = DATE_SUB(cur.month_end, INTERVAL 1 YEAR);
为什么用子查询而不是窗口函数
MySQL 8.0+ 支持 LAG() 和 LEAD(),但财务报表常需跨年、跨部门对齐,而窗口函数默认按 PARTITION BY dept, subject ORDER BY month_end 排序后只能取相邻行——如果某部门某月缺数据,LAG(amount, 1) 会跳到更早的月份,造成同比错位。
性能影响:窗口函数在大数据量下通常更快;但嵌套查询 + 多次 GROUP BY 在未建好复合索引时容易慢。权衡点在于“数据完整性优先级”:报表要 100% 可解释,就宁可慢一点,也要确保 month_end 对齐绝对准确。
- 用
LAG()的前提:确认每个(dept, subject)组内每月都有且仅有一条汇总记录(可用COUNT(*) OVER (PARTITION BY dept, subject, LAST_DAY(trade_date)) > 1检查) - 用嵌套查询时,
LAST_DAY(trade_date)必须出现在所有子查询的GROUP BY和SELECT中,否则JOIN条件无法命中 - 别忘了给
(dept, subject, month_end)建联合索引,否则LEFT JOIN会变全表扫描
除零、空值、跨年天数不一致怎么处理
财务系统最怕计算中突然报 Division by zero 或返回 NULL 却没提示原因。核心原则:所有除法前先用 NULLIF(denominator, 0),所有被减数/减数用 IFNULL(value, 0) 显式兜底,但要注意——“0 增长率”和“无数据”语义不同,不能都填 0。
容易踩的坑:DATE_SUB('2023-02-29', INTERVAL 1 YEAR) 返回 NULL(因为 2022 年没有 2 月 29 日),导致同比 JOIN 断开。解决方案是统一用月末对齐:
- 一律用
LAST_DAY(DATE_SUB(cur.month_end, INTERVAL 1 YEAR))替代裸DATE_SUB - 环比计算中,若上月无数据,
amount_prev为NULL,此时mom_rate应明确设为NULL(不可强制转 0),业务方才知道这是“首月发生” - 用
CASE WHEN prev.amount IS NULL THEN 'NEW' WHEN prev.amount = 0 THEN 'DIV_ZERO' ELSE ... END区分缺失、零值、正常三种状态
财务环比同比看着是数学题,实际是时间对齐题——时间点没卡准,后面全白算。尤其注意月末、年初、跨年这三类边界,它们才是真实生产环境里最常出问题的地方。











