GROUP BY后加同比逻辑须先聚合再开窗,用LAG(SUM(amount),12)配合PARTITION BY和严格ORDER BY实现跨期取值,避免自连接;必须处理NULL和除零,且时间维度需连续或补全。

GROUP BY 后怎么加同比逻辑?
直接在 GROUP BY 之后用 LAG() 窗口函数是最稳的路。因为同比本质是「当前组 vs 上一个同期组」,而 LAG() 能按指定维度(比如年份+月份)把上期值拉到当前行,避免自连接或子查询带来的性能抖动和 NULL 边界问题。
- 必须先按时间字段排序,否则
LAG()拉的不是上期:例如按YEAR(order_date)和MONTH(order_date)双序排 - 分组字段(如
product_category)要同时放进PARTITION BY,否则跨品类混拉 -
LAG()默认取前 1 行,但若数据有缺失(比如某月无销量),需配合ORDER BY的连续性判断,不能假设“上一行就是去年同期”
SELECT
product_category,
YEAR(order_date) AS y,
MONTH(order_date) AS m,
SUM(amount) AS cur_amt,
LAG(SUM(amount), 1) OVER (
PARTITION BY product_category
ORDER BY YEAR(order_date), MONTH(order_date)
) AS last_amt,
ROUND(
(SUM(amount) - LAG(SUM(amount), 1) OVER (
PARTITION BY product_category
ORDER BY YEAR(order_date), MONTH(order_date)
)) / NULLIF(LAG(SUM(amount), 1) OVER (
PARTITION BY product_category
ORDER BY YEAR(order_date), MONTH(order_date)
), 0),
4
) AS yoy_rate
FROM sales
GROUP BY product_category, YEAR(order_date), MONTH(order_date);
为什么不能用 WHERE 过滤后再算同比?
因为 WHERE 是在聚合前过滤,会直接砍掉某些月份的数据,导致 LAG() 拉不到真正的去年同期值——它只认物理上「上一行」,不认日历逻辑。比如 2023-02 缺失,那 2023-03 的 LAG() 就会落到 2023-01,而非 2022-03。
- 正确做法是:先保证时间维度完整(可用日历表
LEFT JOIN补空),再聚合,最后开窗 - 如果不想补全,至少用
DATE_SUB(MIN(order_date), INTERVAL 1 YEAR)扩大原始查询时间范围,确保上期数据存在
NULLIF() 在除法里到底防什么?
防分母为 0 报错,但更关键的是防分母为 NULL 导致整列结果变 NULL。因为 LAG() 对首期(如 2022-01)返回 NULL,若不包一层 NULLIF(..., 0),/ NULL 仍得 NULL,但业务上你可能希望首期显示 NULL 或 0,而不是让整个比率列失效。
- 不要用
COALESCE(LAG(...), 0)替代NULLIF:0 做分母照样炸 - 实际上线建议加
CASE WHEN last_amt IS NULL THEN NULL ELSE ... END显式控制首期行为
MySQL 5.7 没有 LAG 怎么办?
只能用自连接模拟,但代价高、易出错:
- 需对每个分组生成「当前期」和「上年同期」两个别名,连接条件是
t1.y = t2.y + 1 AND t1.m = t2.m AND t1.category = t2.category - 必须用
LEFT JOIN,否则缺上期数据的行直接丢弃 - 聚合必须在外层做,否则连接会放大行数(一对多爆炸)
这种写法在百万级数据上很容易慢三倍以上,且难以加索引优化。如果无法升级 MySQL 版本,优先考虑应用层计算,或者用临时表预存各月汇总再连接。
真正麻烦的不是语法,是时间维度对齐——比如财务年度从 4 月开始、或存在闰年 2 月天数差异,这些都得在 ORDER BY 或连接条件里显式处理,窗口函数只是帮你省了连接步骤,没省掉逻辑校验。

















