group by无法跨行计算,必须用窗口函数:累计求和用sum() over(partition by... order by...),环比用lag/lead配合order by和默认值,滚动计算需区分rows(按行数)与range(按值范围),且三者partition by、order by、frame缺一不可。

GROUP BY 不能直接跨行,得靠窗口函数
SQL 的 GROUP BY 只能聚合当前分组内的行,没法“看到”上一行或下一行的数据。真要跨行计算(比如累计求和、环比、移动平均),必须用窗口函数——OVER() 是唯一靠谱的路径。
常见错误是硬套 GROUP BY + 聚合函数去模拟跨行逻辑,结果要么报错,要么数据错位。比如写 SUM(amount) GROUP BY user_id ORDER BY dt,看似有序,但实际不保证跨时间行的累计效果,因为 GROUP BY 会打乱原始顺序,且不提供行间引用能力。
-
SUM(amount) OVER (PARTITION BY user_id ORDER BY dt)才是正确累计写法:先按user_id分区,再在每个分区内按dt排序,然后逐行累加 - 注意
ORDER BY必须出现在OVER子句里,否则默认无序,累计结果不可控 - 如果分区键和排序键都为空(即
OVER()),就等价于全表范围计算,容易引发性能问题,尤其在大数据量时
LAG/LEAD 处理“上一行”“下一行”的值
当需要取前一行的销售额算环比、或下一行的状态做状态转移判断时,LAG() 和 LEAD() 是最直接的工具。它们本质是偏移访问,不是聚合,但属于典型的跨行场景。
容易踩的坑是忽略默认值设置:不指定第三个参数时,首行的 LAG() 返回 NULL,可能让后续的除法或比较出错。
-
LAG(sales, 1, 0) OVER (PARTITION BY region ORDER BY month)表示取同一region内上一个月的sales,取不到时填0 -
LEAD(status, 1) OVER (ORDER BY event_time)常用于判断用户行为序列中“是否连续登录”,但要注意event_time是否有重复——重复值会导致排序不稳定,建议加上唯一字段如id作为次要排序键 - 偏移步长不一定是
1,比如LAG(value, 7)可取 7 天前的值,适合周同比场景
ROLLING 和 RANGE 窗口帧容易被误用
很多同学以为 ROWS BETWEEN 2 PRECEDING AND CURRENT ROW 就是“最近 3 行”,但在 ORDER BY 有重复值时,ROWS 按物理行数算,RANGE 按排序值范围算——二者行为完全不同。
例如按日期排序,若多行日期相同,RANGE BETWEEN INTERVAL '6' DAY PRECEDING AND CURRENT ROW 会把所有同天及之前 6 天内的行全纳入,而 ROWS BETWEEN 6 PRECEDING AND CURRENT ROW 只取最近 7 条记录,不管日期。
- 时间类滚动计算优先用
RANGE+INTERVAL,更符合业务语义(如“近 30 天销售额”) - 序号类滚动(如“最近 5 笔订单”)必须用
ROWS,否则重复序号会漏或多算 - PostgreSQL 支持
RANGE配INTERVAL,MySQL 8.0+ 也支持,但 SQLite 和旧版 MySQL 不支持RANGE时间偏移,得用自连接或变量模拟
聚合 + 窗口嵌套时的执行顺序很关键
像 AVG(COUNT(*)) OVER (...) 这种写法会报错,因为 COUNT(*) 是聚合函数,不能直接放在窗口函数里——窗口函数作用对象必须是基表列或已计算的标量表达式。
真要实现“每个部门的平均员工数(按月统计后取均值)”,得两层处理:先用子查询或 CTE 按部门+月份算出每组人数,再在外层对这个结果集开窗。
- 错误写法:
AVG(COUNT(emp_id)) OVER (PARTITION BY dept)→ 语法错误 - 正确写法:先
SELECT dept, month, COUNT(*) AS cnt FROM t GROUP BY dept, month,再AVG(cnt) OVER (PARTITION BY dept) - 某些引擎(如 BigQuery)允许
ARRAY_AGG+UNNEST绕过限制,但可读性和维护性差,不推荐作为常规方案
跨行计算的本质是定义清楚“当前行的上下文范围”,而不是堆砌函数。窗口子句里的 PARTITION BY、ORDER BY、FRAME 三者缺一不可,少写一个就可能让结果完全偏离预期。











