同比计算应优先使用DATE_SUB或INTERVAL进行日期偏移,避免字符串拼接;需确保业务周期对齐(如用period_id而非年月加减),并用CASE WHEN处理分母为0或NULL的情况。

用 DATE_SUB 或 INTERVAL 做同比日期偏移,别硬写字符串拼接
同比就是“今年某月 vs 去年同月”,核心是把当前日期减去 1 年。MySQL 里最稳的方式是 DATE_SUB(curdate(), INTERVAL 1 YEAR),PostgreSQL 用 current_date - INTERVAL '1 year'。硬拼 CONCAT(YEAR(date_col)-1, '-', MONTH(date_col), '-', DAY(date_col)) 看似直观,但遇到 2 月 29 日或月末(如 3 月 31 日 → 2 月 29 日不存在)会直接报错或结果错乱。
实操建议:
- 优先用数据库原生日期运算函数,不是字符串操作
- 如果字段是
datetime类型,偏移后注意时区和精度,必要时用DATE()截断时间部分 - 在 WHERE 条件里做偏移时,确保该列有索引,否则
DATE_SUB(sale_date, INTERVAL 1 YEAR)无法走索引
自关联聚合算同比增长率:ON 条件必须对齐业务周期
常见错误是写成 ON t1.month = t2.month AND t1.year = t2.year + 1——这只能对齐“自然月”,但实际业务可能按财年、滚动 30 天、或销售周期(比如 4-4-5 周历)。真正要对齐的是“同一业务周期标识”,比如你有一张 dim_period 表带 period_id 和 is_current 字段,那关联条件应该是 ON t1.period_id = t2.last_year_period_id。
实操建议:
- 不要依赖年份/月份字段直接加减,先确认业务上“同比”定义是否和日历一致
- 若无维度表,至少用
YEARWEEK(sale_date, 1)(MySQL)对齐周粒度,避免跨周偏差 - LEFT JOIN 后记得用
COALESCE(t2.amount, 0)处理去年无数据的情况,否则除零或 NULL 会让整行消失
计算公式里分母为 0 或 NULL 的坑必须显式处理
同比增长率公式是 (this_year - last_year) / last_year。一旦 last_year 是 0 或 NULL,结果就是 NULL 或报错(如 PostgreSQL 的 division by zero)。这不是数学问题,是 SQL 执行逻辑问题。
实操建议:
- 永远用
CASE WHEN COALESCE(last_year, 0) = 0 THEN NULL ELSE (this_year - last_year) / last_year END - 别信
NULLIF(last_year, 0)——它只解决除零,不解决 NULL 分母;COALESCE更可控 - 如果业务要求“去年为 0 时显示 100%”,那就明确写
WHEN last_year = 0 AND this_year > 0 THEN 1.0
窗口函数替代自关联?小心数据膨胀和排序依赖
有人想用 LAG(amount) OVER (PARTITION BY product_id ORDER BY sale_month) 算同比,但 LAG 默认是“上一行”,不是“去年同期”。除非你提前把数据按年月补全(比如用 generate_series 或递归 CTE),否则 LAG 拿到的大概率是上个月,不是去年同月。
实操建议:
- LAG/LEAD 适合环比,不适合同比;强行用就得先保证每期数据严格存在、且排序字段能唯一映射到“去年同周期”
- 如果真要用窗口函数,得配合
ROW_NUMBER() OVER (PARTITION BY product_id, YEAR(sale_date) ORDER BY sale_date)再做二次匹配,复杂度远超自关联 - 大表慎用自关联+窗口混用,容易触发临时表磁盘溢出,尤其在 MySQL 8.0 以下
同比看着简单,实际卡点全在“周期对齐”和“空值防御”上。业务口径没对齐,代码写得再漂亮,结果也是错的;分母没兜底,报表一跑就断。










