用LAG()获取同比值需按年月标识排序并偏移12行,但数据缺失时易错位;更稳妥的是用日期运算关联去年同月聚合结果,注意时区、闰年及NULL处理。

如何用 LAG() 和日期运算获取同比值
直接用 LAG() 拿上一年同月数据最稳妥,但必须确保分组和排序能对齐“年-月”粒度。常见错误是只按 ORDER BY date 排序,没考虑跨年时月份错位——比如 2023-01 的前一行可能是 2022-12,而非 2022-01。
正确做法是先提取年月标识(如 YEAR(date) * 100 + MONTH(date) 或 DATE_FORMAT(date, '%Y%m')),再按该字段排序:
SELECT DATE_FORMAT(order_date, '%Y-%m') AS ym, SUM(amount) AS monthly_total, LAG(SUM(amount), 12) OVER (ORDER BY DATE_FORMAT(order_date, '%Y%m')) AS last_year_same_month FROM orders GROUP BY DATE_FORMAT(order_date, '%Y-%m') ORDER BY ym;
- 窗口函数的
ROWS BETWEEN不适用此处,LAG()的偏移量必须是固定行数(12 行),不是固定时间间隔 - 如果数据存在缺失月份(如 2022-06 没订单),
LAG(..., 12)会跳过空组,取到的是更早的非空月,导致同比错位——必须补全月份或改用日期计算 - MySQL 8.0+、PostgreSQL、SQL Server 2016+ 支持该写法;SQLite 不支持窗口函数,需用自连接替代
用日期运算替代行偏移来规避数据稀疏问题
当月度数据不连续时,靠 LAG(..., 12) 取值不可靠,应改为关联“去年同月”的聚合结果。核心是构造一个可 join 的时间键:
WITH monthly AS (
SELECT
DATE_FORMAT(order_date, '%Y-%m') AS ym,
SUM(amount) AS total
FROM orders
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
)
SELECT
cur.ym,
cur.total,
last.total AS last_year_total,
ROUND((cur.total - last.total) / NULLIF(last.total, 0), 4) AS yoy_rate
FROM monthly cur
LEFT JOIN monthly last
ON last.ym = DATE_FORMAT(DATE_SUB(STR_TO_DATE(CONCAT(cur.ym, '-01'), '%Y-%m-%d'), INTERVAL 1 YEAR), '%Y-%m');
-
STR_TO_DATE(CONCAT(cur.ym, '-01'), '%Y-%m-%d')是为了把字符串 '2023-06' 转成日期再减一年,避免字符串直接拼接出错 - PostgreSQL 应用
(cur.ym::date - INTERVAL '1 year')::text[:7];SQL Server 用FORMAT(DATEADD(YEAR, -1, CAST(cur.ym + '-01' AS DATE)), 'yyyy-MM') - 注意
NULLIF(last.total, 0)防止除零,但若去年同月为 0,同比变化本身无意义,需业务侧明确是否展示 Inf/-Inf
为什么不能直接用 DATE_SUB(date, INTERVAL 1 YEAR) 在窗口里做偏移
窗口函数作用在当前行的上下文,无法动态改变分区边界去匹配“去年同月”。例如以下写法是错的:
-- ❌ 错误:窗口函数不接受表达式作为偏移量 LAG(total, DATE_SUB(date, INTERVAL 1 YEAR)) OVER (...)
因为 LAG() 第二个参数必须是常量整数,不是计算结果。有人试图用 ROW_NUMBER() 配合日期差筛选,但逻辑复杂且易出错——不如用显式 join 或 CTE 预聚合。
- 试图用
UNBOUNDED PRECEDING加条件过滤,语法不支持 - 某些引擎(如 BigQuery)提供
OFFSET结合ARRAY_AGG模拟,但可读性差、性能低,不推荐用于常规场景 - 真正需要动态时间偏移的,应考虑物化视图预生成“年-月-去年同期”映射表
实际部署时最容易被忽略的细节
生产环境跑同比,往往卡在两个地方:一是时区未统一,前端传的日期和数据库存储时区不一致,导致 DATE_FORMAT 结果偏差;二是没有处理闰年 2 月的边界情况(如 2024-02 对应 2023-02,但 2023-02 只有 28 天,而订单日期可能落在 29 日)。
- 所有日期函数调用前,确认会话时区:
SELECT @@time_zone(MySQL)或CURRENT_SETTING('timezone')(PostgreSQL) - 用
LAST_DAY()替代固定加减日数,例如计算“去年同月最后一天”:LAST_DAY(DATE_SUB(STR_TO_DATE('2024-02-01', '%Y-%m-%d'), INTERVAL 1 YEAR)) - 如果原始数据含时间戳,务必先
DATE(order_time)截断,否则跨日聚合会导致同一天被拆到不同月份










