必须用SUM() OVER()窗口函数实现分组内移动总计,核心是PARTITION BY分组、ORDER BY排序、ROWS BETWEEN指定范围;禁用GROUP BY混用,避免全表扫描需为ORDER BY字段建索引。

SQL里用SUM()做分组内移动总计,必须配WINDOW子句
不加OVER()的SUM()是聚合函数,会把整组压成一行;要保留原始行数并逐行累加,必须用窗口函数。核心是写对OVER()里的排序和范围——没ORDER BY就谈不上“移动”,没ROWS BETWEEN就默认是UNBOUNDED PRECEDING TO CURRENT ROW(从头累加到当前行)。
- 分组内计算,
PARTITION BY放最外层,比如按user_id分组就写PARTITION BY user_id -
ORDER BY必须存在且有意义,比如按order_date或created_at升序,否则“移动”无方向 - 如果只要最近3条记录的累计值,得显式写
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW(注意:2 PRECEDING 表示往前数2行,共3行) - MySQL 8.0+、PostgreSQL、SQL Server 2012+、Oracle 都支持;SQLite 3.25+ 也行,但旧版不支持
常见错误:GROUP BY 和 OVER() 混用导致结果错乱
有人会写SELECT user_id, SUM(amount) OVER(...), COUNT(*) FROM orders GROUP BY user_id,这会报错或语义错误——GROUP BY和窗口函数在同一个SELECT里共存时,非聚合/非窗口字段必须出现在GROUP BY中,且窗口计算是在GROUP BY之后执行的。真要分组+移动总计,通常不需要GROUP BY,靠PARTITION BY就够了。
- 错误现象:
ERROR: column "amount" must appear in the GROUP BY clause或结果行数变少 - 正确做法:去掉
GROUP BY,用PARTITION BY user_id ORDER BY order_date替代 - 如果还要统计每组总条数,用
COUNT(*) OVER(PARTITION BY user_id),别混GROUP BY
指定窗口范围的写法差异:ROWS vs RANGE
ROWS按物理行数定位,RANGE按排序值的逻辑范围定位。多数场景该用ROWS,尤其当排序字段有重复值时——RANGE会把同值的所有行一起纳入窗口,容易多算。
- 例如按
order_date排序,两天订单量相同,RANGE BETWEEN INTERVAL '7 days' PRECEDING AND CURRENT ROW可能拉进来10天的数据 - 安全写法:统一用
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW表示“最近7天(含当天)”的近似,前提是数据每天一条且不缺漏 - PostgreSQL 支持
RANGE BETWEEN '7 days' PRECEDING AND CURRENT ROW,但MySQL 不支持带单位的RANGE,只能用ROWS
性能提醒:ORDER BY 字段一定要有索引
窗口函数执行时,数据库需要按ORDER BY字段快速定位前后行。没索引的话,大表上SUM() OVER(PARTITION BY x ORDER BY y ROWS ...)可能全表扫描+临时排序,延迟陡增。
- 复合索引优先建
(x, y),比如(user_id, created_at),能同时支撑分组和排序 - 检查执行计划:PostgreSQL 看是否有
WindowAgg节点配合Index Scan;MySQL 看Extra列是否含Using filesort - 如果只是求“每组前N行累计”,可先用
ROW_NUMBER() OVER(...)过滤,再套一层SUM,比直接大窗口更省资源
实际用的时候,最容易被忽略的是ORDER BY缺失或字段无索引——看起来语法对了,一跑大数据量就卡住,还以为是SQL写错了。










