用LAG()窗口函数可一次性计算同比环比:LAG(value,1)按年月排序得环比,LAG(value,12)得同比,需补全时间维度、标准化年月键,并处理NULL场景;大表建议物化聚合结果提升性能。

用窗口函数一次性算出同比和环比,别写多个子查询
直接在主查询里嵌套多个 LAG() 和 LEAD() 就能拿到上期、去年同期值,避免反复 JOIN 或 WITH 子句拉长执行链。关键不是“能不能”,而是“怎么让分区和排序刚好对齐业务周期”。
常见错误是按自然月分组后直接 LAG(value) OVER (ORDER BY month),结果发现 2024-01 的“上期”变成 2023-12 —— 没问题,但“去年同期”如果也只靠 ORDER BY month,就无法跳过整年。必须显式按年+月双层排序,或用日期运算构造锚点。
-
LAG(value, 1) OVER (PARTITION BY product_id ORDER BY year, month)→ 环比(同产品、相邻自然月) -
LAG(value, 12) OVER (PARTITION BY product_id ORDER BY year, month)→ 同比(同产品、12个月前),前提是数据按年月完整连续 - 如果存在缺失月份(比如某产品 2023-06 无数据),
LAG(value, 12)会取到 2023-05,而非真正的 2023-06;此时应先用GENERATE_SERIES(PostgreSQL)或递归 CTE 补全时间维度
日期字段必须标准化为年月粒度,别依赖字符串截取
用 TO_CHAR(order_date, 'YYYY-MM') 或 DATE_TRUNC('month', order_date) 统一生成时间键,比 SUBSTR(order_date::text, 1, 7) 更可靠 —— 后者在 order_date 是 TIMESTAMP WITH TIME ZONE 且跨时区时可能错位。
尤其注意:SQL Server 的 FORMAT(order_date, 'yyyy-MM') 返回字符串,参与 ORDER BY 时会按字典序排,2024-10 会排在 2024-2 前面。必须转回日期类型再截断,例如 DATEFROMPARTS(YEAR(order_date), MONTH(order_date), 1)。
- PostgreSQL 推荐:
DATE_TRUNC('month', order_date),类型仍是TIMESTAMP,可直接用于窗口排序 - MySQL 8.0+:用
YEAR(order_date) * 100 + MONTH(order_date)构造整数年月码,避免字符串比较陷阱 - BigQuery:用
EXTRACT(YEAR FROM order_date) * 100 + EXTRACT(MONTH FROM order_date),和 MySQL 逻辑一致
增长率为 NULL 的三种典型场景及应对
计算 (cur_value - prev_value) / prev_value 时,NULL 不是 bug,是信号:它暴露了数据断层、零值分母或分区边界问题。
- 上期值为
NULL→ 当前记录是该分组第一条数据(如新上线产品首月),此时环比无意义,建议显式标为'N/A'而非留空 - 上期值为
0→ 除零报错或得INF,需前置判断:CASE WHEN prev_value = 0 THEN NULL ELSE (cur_value - prev_value) / prev_value END - 同比值为
NULL但环比有值 → 很可能去年同月无数据(如去年未运营),不能简单用 0 替代,否则扭曲增长率分布;应保留 NULL 并在 BI 层过滤或打标
大表上加多层 LAG 可能拖慢查询,优先考虑物化中间结果
当基础表超千万行,且要同时算 5 个指标(如销售额、订单量、客单价的同比+环比),每个 LAG 都触发一次全表排序,CPU 和内存压力陡增。这时不如把“年月+维度键+聚合值”先存成临时表或物化视图。
以 PostgreSQL 为例:CREATE MATERIALIZED VIEW mv_monthly_metrics AS SELECT product_id, DATE_TRUNC('month', order_date) AS stat_month, SUM(amount) AS sales FROM orders GROUP BY product_id, DATE_TRUNC('month', order_date);,后续所有 LAG 都在这个轻量级视图上跑,性能提升常达 3–5 倍。
- MySQL 没有物化视图,可用
CREATE TABLE ... AS SELECT+ 定时任务刷新 - ClickHouse 建议直接建
ReplacingMergeTree表预聚合,避免每次查都重算 - 千万别在原始明细表上直接套 3 层嵌套窗口函数查百万级用户日志 —— 先按天/小时聚合再算增长
真正难的不是写出公式,而是确认“同比”到底指日历同比、财年同比,还是滚动 365 天同比;这些语义差异不会报错,但会让结果偏离业务预期。

















