同比计算必须匹配同一周期的去年值,lag()仅取上一行易错位;正确做法是用date_sub或年月字段对齐后lag(12),或自连接按年月精确关联,并补全缺失月份。

同比增长率的计算逻辑必须对齐时间维度
直接用 LAG() 算同比,结果大概率是错的——因为同比要求「同一周期的去年值」,而 LAG(col, 1) 只取上一行,不保证是去年同月/同日。比如 2024-03 的数据,需要匹配 2023-03,不是 2024-02。窗口函数本身不理解“年”,得靠排序和偏移量共同约束。
常见错误现象:LAG(sales, 1) OVER (ORDER BY dt) 在按日聚合时,会把 2024-01-02 和 2024-01-01 当作同比关系;在按月聚合但未补全月份(如 2023-02 缺失)时,LAG 会跳到 2023-01,导致错位。
- 正确做法:先确保数据按「年+月」或「年+周」等周期对齐,再用
LAG(..., 12)(月度)或LAG(..., 52)(周度) - 更稳妥的做法:用
DATE_SUB(dt, INTERVAL 1 YEAR)构造去年日期,再通过自连接或JOIN LATERAL关联 - 注意时区与日期截断:用
YEAR(dt)和MONTH(dt)拆分后 join 更稳定,避免跨闰年、月末差异(如 2023-02-28 → 2024-02-29 不存在)
用窗口函数实现月度同比增长(需预处理年月字段)
核心是让每行能拿到「自身年月 - 1 年」对应的值,LAG 在固定周期下最简洁,但前提是数据已按月完整、连续。
SELECT
ym,
sales,
LAG(sales, 12) OVER (ORDER BY ym) AS last_year_sales,
ROUND((sales - LAG(sales, 12) OVER (ORDER BY ym)) * 100.0 / NULLIF(LAG(sales, 12) OVER (ORDER BY ym), 0), 2) AS yoy_pct
FROM (
SELECT
DATE_FORMAT(order_date, '%Y-%m') AS ym,
SUM(amount) AS sales
FROM orders
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
) t;
关键点:
-
DATE_FORMAT(order_date, '%Y-%m')确保排序单位是「年月字符串」,比直接ORDER BY order_date安全 -
NULLIF(..., 0)防止除零;同比分母为 0 或 NULL 时,结果自动为 NULL,不报错 - 如果某月缺失(如 2023-06 无数据),
LAG(sales, 12)会跳过它取更早的值,所以必须先补全月份(用generate_series或日历表)
用自连接处理不规则或稀疏时间数据
当原始数据按天记录、且存在大量空缺日期时,自连接比窗口函数更可控——你明确指定关联条件,而不是依赖行序。
SELECT cur.ym, cur.sales, last.sales AS last_year_sales, ROUND((cur.sales - last.sales) * 100.0 / NULLIF(last.sales, 0), 2) AS yoy_pct FROM monthly_summary cur LEFT JOIN monthly_summary last ON cur.year = last.year + 1 AND cur.month = last.month;
这里 monthly_summary 是一个带 year 和 month 列的中间表(可由 CTE 或物化视图生成)。优势在于:
- 不受数据是否连续影响:即使 2023-04 缺失,2024-04 仍会 LEFT JOIN 到 NULL,不会错配
- 可轻松扩展条件:比如只比工作日、排除促销期、加行业均值基准等,都只需改
ON或加WHERE - MySQL 8.0+ 支持
JOIN LATERAL,能替代部分自连接场景,但语义更重,调试成本略高
性能与兼容性取舍:窗口函数快,但自连接更稳
在千万级订单表上,LAG 版本通常快 2–3 倍(单次扫描 + 窗口排序);但一旦业务要求「任意时间范围同比」(如用户选 2024-Q2 vs 2023-Q2),自连接配合分区裁剪反而更容易优化。
- PostgreSQL / ClickHouse:优先用
LAG+GENERATE_SERIES补月,再窗口计算 - MySQL 5.7:不支持窗口函数,只能自连接或应用层拼接
- ClickHouse 注意:
toYYYYMM(date)比字符串格式化快,且LAG在ORDER BY toYYYYMM(date)下行为确定 - 别忽略 NULL 处理:同比率为负数时,
ROUND(..., 2)会保留 -0.00,前端显示可能需额外 trim
真正麻烦的从来不是写法,而是确认「同比」在你业务里到底指什么——是自然年?财年?滚动 12 个月?这些定义一旦模糊,再准的 SQL 也算不准。










