错误做法是直接在WHERE中用YEAR(NOW())-1和MONTH(NOW())计算同比,导致无法批量处理、丢数据、索引失效、时区错乱;正确做法是用DATE_FORMAT生成年月键,通过子查询关联去年聚合结果,确保LEFT JOIN不丢失空月份。

直接用 WHERE year = YEAR(NOW()) - 1 AND month = MONTH(NOW()) 算同比,基本等于没算——它只查固定一个月,没法批量跑全表,还容易丢数据。
为什么不能在 WHERE 里硬减年份
常见错误是把同比理解成“去年这个时间点”,于是写 WHERE order_date >= DATE_SUB('2024-03-01', INTERVAL 1 YEAR)。问题在于:一旦某个月(比如 2023 年 2 月)没有订单记录,整个 2024 年 2 月的同比值就没了,LEFT JOIN 变成 INNER JOIN 效果,报表断层。
- 日期函数在
WHERE中无法利用索引,尤其DATE_SUB(order_date, INTERVAL 1 YEAR)这类表达式,MySQL 会放弃走order_date上的索引 - 如果原始表只有
year_num和month_num字段(非日期类型),YEAR(NOW()) - 1无法和字段直接比对,类型不一致导致隐式转换,索引失效 - 时区没对齐时,
NOW()返回本地时间,但表中order_date是 UTC 存储,2024-03-01 00:00:00 UTC 对应北京时间是 3 月 1 日上午 8 点,漏掉前 8 小时数据
正确做法:用子查询生成可 JOIN 的同比键
核心不是“找去年数据”,而是“把今年每条记录对应的去年同月,变成一个字符串字段”,再拿这个字段去关联去年聚合结果。这样哪怕去年某月无数据,LEFT JOIN 也会返回 NULL,你还能补 0 或标“N/A”。
- 主查询里统一用
DATE_FORMAT(order_date, '%Y-%m')提取年月,别用CONCAT(YEAR(), '-', LPAD(MONTH(), 2, '0'))——后者在某些 MySQL 版本里可能因执行顺序出错 - 去年子查询必须独立限定时间范围,例如
WHERE order_date >= '2023-01-01' AND order_date ,不能只靠 JOIN 条件兜底,否则去年数据会漏 - JOIN 条件写成
curr.month_str = last.yoy_month,其中last.yoy_month是去年子查询里提前算好的:CONCAT(YEAR(order_date) + 1, '-', DATE_FORMAT(order_date, '%m')) AS yoy_month - 务必给
DATE_FORMAT(order_date, '%Y-%m')建函数索引(MySQL 8.0+)或冗余一个ym_str CHAR(7)字段并建普通索引,否则全表扫描
计算同比增幅时最容易踩的坑
写 (curr.total_amt - last.total_amt) / last.total_amt 看似标准,但只要 last.total_amt 是 0 或 NULL,结果就崩——MySQL 返回 NULL,PostgreSQL 直接报 division by zero 错误。
- 分母必须套
NULLIF(last.total_amt, 0),把 0 转成 NULL,避免除零 - 分子也要
COALESCE(curr.total_amt, 0),否则今年没数据时,减法结果为 NULL,整个增幅变 NULL - 乘 100.0 而不是 100,否则整数除法在 PostgreSQL/SQL Server 里会截断为 0(比如 5/10 得 0,不是 0.5)
- 最后用
ROUND(..., 4)控制小数位,别依赖应用层格式化,数据库里定死更可控
真正难的不是写出能跑的 SQL,而是让每个 month_str 都有确定的、可复现的“去年同月”映射——这要求你从第一行数据开始就用相同逻辑构造键,而不是靠函数临时推算。一旦子查询里拼错 CONCAT 或漏了 LPAD 补零,'2023-1' 和 '2023-01' 就永远对不上。

















