用LAG()/LEAD()计算环比同比更高效,需严格按时间排序并明确分区;LAG(sales_amount,1)取逻辑上期,非日历上期;同比应按月日分组而非固定偏移12行;注意NULL、零值及数据连续性校验。

用 LAG() 和 LEAD() 直接算环比,别再写自连接
环比(比上期)本质是同一指标在相邻时间点的差值或比率,传统做法常靠子查询或自连接关联上月数据,性能差、可读性低。窗口函数 LAG() 能直接“回头看一行”,省掉关联逻辑。
关键点:必须按时间严格排序,且分区(如按产品线分组)要明确,否则 LAG() 会跨组取值。
-
LAG(sales_amount, 1) OVER (PARTITION BY product_id ORDER BY report_date)—— 取本产品上一报告期销售额 - 若日期非自然连续(比如跳过节假日),
LAG()仍按排序后行序取,不是按日历差;需确认业务是否接受“逻辑上期”而非“日历上期” - 首期数据
LAG()返回NULL,建议用COALESCE(..., 0)或显式过滤,避免除零或空值传播
同比要用 LAG() 配合时间偏移,不是简单加 12
同比(比去年同期)不是固定偏移 12 行,而是匹配“同月同日但年份减 1”。如果数据按月粒度(report_month CHAR(7) 或 DATE),得先确保排序字段能天然对齐同比周期。
常见错误:直接 LAG(value, 12) —— 这只在每月一条、无缺失、严格连续时才等价于同比,一旦缺月或有多条记录就错。
- 安全做法:用
LAG(value) OVER (PARTITION BY EXTRACT(MONTH FROM report_date), EXTRACT(DAY FROM report_date) ORDER BY report_date),按月日分组再排序 - 更通用:构造同比基准字段,如
DATE_SUB(report_date, INTERVAL 1 YEAR),再用LEFT JOIN关联——但这就脱离了纯窗口函数场景;纯窗口解法依赖数据规律性 - 注意时区和
DATE类型精度:若report_date含时分秒,EXTRACT(DAY)没问题,但跨夏令时可能影响DATE_SUB结果
一次查询产出同比+环比+完成率,SELECT 里叠窗口表达式没问题
多个窗口函数可共存于同一 SELECT,只要它们的 OVER 子句互不干扰。不需要拆成多层子查询,也不会重复扫描表。
性能取决于底层数据量和排序开销,但比多次关联或 CTE 更轻量。
- 示例字段组合:
sales_amount,LAG(sales_amount) OVER w_m AS last_month,LAG(sales_amount, 12) OVER w_y AS last_year,ROUND((sales_amount - last_month)/NULLIF(last_month, 0), 4) AS mom_rate - 定义复用窗口:可用
WINDOW w_m AS (PARTITION BY region ORDER BY report_date)简化写法(PostgreSQL/MySQL 8.0+ 支持) - Oracle 用户注意:
ROUND对NULL安全,但NULLIF在部分旧版本不支持,可用CASE WHEN last_month = 0 THEN NULL ELSE ... END
NULL 值和边界情况比想象中更容易崩报表
窗口函数本身不报错,但下游计算(尤其是除法、百分比)遇到 NULL 或零值,会导致整行结果不可用,而你可能只看到一片空白或 NaN,却没意识到是首期/缺期/归零导致。
- 强制检查:在最终
SELECT前加WHERE report_date >= '2023-02-01'(排除环比无基期的数据) - 同比字段若依赖
LAG(..., 12),则至少要有 13 条连续记录才能有首个有效同比值;实际业务中往往前 N 个月都该设为NULL或标记为“不可比” - 导出到 Excel 或 BI 工具时,
NULL可能被转成空字符串或 0,掩盖逻辑缺陷;建议在 SQL 层用CASE WHEN last_month IS NULL THEN 'N/A' ELSE ... END显式标注状态
真正麻烦的不是写不出公式,而是默认假设数据完美连续——现实里缺日、补录、调整单会让窗口函数“看错人”。先用 ROW_NUMBER() OVER (...) 打印行序,核对几条典型数据的前后关系,比调半天 BI 度量靠谱得多。

















