用 LAG() 算同比最省事的方法是先用 DATE_TRUNC('month', dt) 或 TO_CHAR(dt, 'YYYY-MM') 生成统一月份锚点,再按该锚点 ORDER BY;勿在 PARTITION BY 中加业务维度,避免跨年缺失导致 NULL,需用 COALESCE 处理;闰日、时区、除零均需专项防护。

怎么用 LAG() 算同比,而不是写两遍子查询
直接用 LAG() 拿上一年同月数据最省事,前提是日期字段能对齐——比如都是每月 1 号,或者都用 DATE_TRUNC('month', ...) 归一化。否则 LAG(value, 1) OVER (PARTITION BY EXTRACT(YEAR FROM dt), EXTRACT(MONTH FROM dt) ORDER BY dt) 这种写法会错乱,因为没按自然年月排序。
实操建议:
- 先用
DATE_TRUNC('month', order_date)或TO_CHAR(order_date, 'YYYY-MM')生成统一的月份锚点 -
LAG()的ORDER BY必须是这个锚点,不是原始时间戳 - 别在
PARTITION BY里塞业务维度(如product_id),除非你真要每个商品单独比自己去年同月——多数报表需要的是「全量 vs 全量」,所以只按月份排序即可 - 如果源数据有跨年缺失(比如 2023-02 有数,2022-02 没数),
LAG()返回NULL,得用COALESCE(lag_value, 0)防崩
遇到 date_part: timestamp with time zone out of range 怎么办
这是 PostgreSQL 常见报错,典型场景:用 current_date - INTERVAL '1 year' 算去年同日,但碰到 2024-02-29 这种闰日,减一年变成 2023-02-29 —— 不存在,直接炸。
安全做法是绕开“日级相减”,改用年份偏移:
- 用
MAKE_DATE(EXTRACT(YEAR FROM dt)::int - 1, EXTRACT(MONTH FROM dt)::int, 1)先落到当月 1 号,再加间隔(如INTERVAL '1 month' * (EXTRACT(MONTH FROM dt) - 1))——太绕,不推荐 - 更稳的是:用
TO_CHAR(dt, 'YYYY-MM')截出年月,转成整数减 100(如 202402 → 202302),再转回日期:TO_DATE((EXTRACT(YEAR FROM dt)::int * 100 + EXTRACT(MONTH FROM dt)::int - 100)::text, 'YYYYMM') - MySQL 用户注意:
DATE_SUB(dt, INTERVAL 1 YEAR)同样踩闰日坑,得换STR_TO_DATE(CONCAT(YEAR(dt)-1, '-', LPAD(MONTH(dt),2,'0'), '-01'), '%Y-%m-%d')
同比计算结果不准,查半天发现是时区和 CURRENT_DATE 搞的鬼
报表跑在 UTC 服务器,但业务时间按东八区算,CURRENT_DATE 返回的是 UTC 当天(比如 UTC 是 2024-03-01,北京时间还是 2024-02-29),导致「本月」切片错位,同比自然对不上。
关键动作:
- 所有日期过滤、截断、分组,统一用带时区的字段,比如
order_time AT TIME ZONE 'Asia/Shanghai' - 别信
CURRENT_DATE,改用CURRENT_TIMESTAMP AT TIME ZONE 'Asia/Shanghai'::date - 窗口函数里的
ORDER BY如果依赖时间排序,必须确保参与排序的列已转为业务时区,否则LAG()可能拿错行 - 测试时手动代入两个明确日期(如
DATE '2024-02-01'和DATE '2023-02-01')跑一遍,比对着当前时间调更容易定位偏移
为什么 ROUND((cur - last) / NULLIF(last, 0), 4) 有时返回 NaN 或负无穷
除零保护用了 NULLIF(last, 0),但没防 last 是 NULL 或负数。当去年值为 NULL,整个表达式变成 (cur - NULL) / NULL → NULL;当去年是负数且今年是正数,同比增幅可能超 100%,但数值本身合法——真正危险的是去年为 0 且今年非 0,此时 NULLIF(0,0) 返回 NULL,除法不报错但结果是 NULL,后续 ROUND(NULL, 4) 还是 NULL。
稳妥写法:
- 显式处理空值:
NULLIF(last, 0) IS NOT NULL AND last != 0才算同比 - 把分子分母都转成
NUMERIC类型,避免隐式转换引入精度误差 - 显示标注口径:
CASE WHEN last = 0 THEN 'new' WHEN last - 前端展示时,别直接渲染
NULL,统一显示-或—,避免用户误以为是 0
同比看着简单,真正上线后最常翻车的不是逻辑,而是日期对齐、时区漂移、空值传播这三块——每一块都得在 SQL 里显式掐断,不能靠“应该不会出现”蒙混过关。










