分组内环比与同比的典型场景是在多维时间序列分析中,对每个业务分组(如region、product_type)分别计算按时间排序的上期值对比(环比)和去年同期值对比(同比),必须使用窗口函数保留明细行并严格对齐时间粒度,同时处理空值与除零问题。

什么是分组内环比与同比的典型场景
你遇到的其实是多维时间序列分析中很常见的需求:在每个业务分组(比如 region、product_type)内部,既要算「上期值对比」(环比),也要算「去年同期值对比」(同比)。关键约束是:不能先 GROUP BY 再聚合丢掉明细,因为环比/同比都需要访问相邻行的原始值。
窗口函数是唯一合理解法——它保留每行数据的同时,允许跨行取值。但直接套用 LAG() 和 LEAD() 容易出错,核心在于时间粒度对齐和空值处理逻辑不一致。
用 LAG() 实现分组内环比,必须指定 ORDER BY 和 PARTITION BY
环比本质是同一分组内按时间排序后,当前行与前一行的比值。漏掉任一条件都会导致跨组取值或顺序错乱:
-
PARTITION BY region, product_type:确保只在相同分组内找“上期” -
ORDER BY year_month(或date):时间字段必须可排序,且不能是字符串格式的'2024-01'(需转为日期或保证字典序正确) - 如果时间有缺失(比如某月无数据),
LAG(value, 1)会跳过空行取更早的非空值,这通常不是你想要的——应先补全时间维度再开窗
示例:
SELECT region, year_month, sales, ROUND(sales / NULLIF(LAG(sales) OVER (PARTITION BY region ORDER BY year_month), 0), 3) AS mom_ratio FROM sales_data;
同比需要手动对齐年份偏移,LAG() 不够用
LAG(sales, 12) 看似能取去年同月,但前提是数据严格按月连续、无缺失。现实中只要某个月缺数,LAG(sales, 12) 就会取错行(比如 2024-03 的值可能来自 2023-02 而非 2023-03)。
更可靠的做法是:用 JOIN 或子查询先生成「当前行对应去年同期的时间键」,再关联取值。但若坚持纯窗口解法,可用 MAX() OVER 配合条件过滤模拟:
SELECT
region,
year_month,
sales,
-- 找到同一 region 下,year_month - INTERVAL '12 months' 对应的 sales 值
MAX(CASE WHEN year_month = ADD_MONTHS(current_year_month, -12) THEN sales END)
OVER (PARTITION BY region ORDER BY year_month ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS yoy_base
FROM (
SELECT
region,
year_month,
sales,
-- 把 year_month 转成日期用于计算
TO_DATE(year_month || '-01', 'YYYY-MM-DD') AS current_year_month
FROM sales_data
) t;注意:ADD_MONTHS 是 Oracle/StarRocks 语法,PostgreSQL 用 current_year_month - INTERVAL '12 months',MySQL 用 DATE_SUB(current_year_month, INTERVAL 12 MONTH)。
合并环比与同比时,NULLIF 和除零必须显式处理
两个比率计算都面临分母为 0 或 NULL 的问题。直接写 sales / LAG(sales) 会导致结果为 NULL(因任一操作数为 NULL),而除零会报错(如 PostgreSQL)或返回 Infinity(如某些引擎):
- 用
NULLIF(denominator, 0)把 0 分母转成 NULL,再让整个除法结果为 NULL - 不要依赖数据库默认行为——不同 SQL 引擎对
0/0、1/0的处理差异极大 - 如果业务要求用 0 代替 NULL,需额外套
COALESCE(..., 0),但要确认这是否符合统计口径
最终合并计算建议写成:
SELECT
region,
year_month,
sales,
ROUND(sales / NULLIF(LAG(sales) OVER w, 0), 4) AS mom_ratio,
ROUND(sales / NULLIF(
MAX(CASE WHEN year_month = yoy_month THEN sales END) OVER w, 0
), 4) AS yoy_ratio
FROM (
SELECT
region,
year_month,
sales,
TO_CHAR(TO_DATE(year_month || '-01', 'YYYY-MM-DD') - INTERVAL '12 months', 'YYYY-MM') AS yoy_month
FROM sales_data
) t
WINDOW w AS (PARTITION BY region ORDER BY year_month);真正难的不是写法,而是确保 year_month 字段本身没有脏数据——比如混入 '2024-Q1' 或 '2024.01' 这类非标准格式,会导致 TO_DATE 失败或对齐错位。

















