SQL 的窗口函数(Window Function)是一类”跨行计算但保留明细”的函数,它借助 OVER() 子句在每行上附加一段相关行的统计或排序结果,而不像 GROUP BY 那样把多行压缩成一行。聚合函数搭配 GROUP BY 后,每组只返回一条汇总记录、原始明细消失;窗口函数则让每行都保留,并在行尾追加一份分组上下文。据 ConversationalSQL 的概括,窗口函数”在保留每一行的同时对其做汇总”,这正是它区别于聚合函数的根本。MySQL 8.0+、SQL Server、PostgreSQL、Oracle 均支持窗口函数。
一、窗口函数的本质:加上下文,不折叠行
聚合函数的目标是”收口”——按 GROUP BY 把同组行合并成一个汇总行。窗口函数相反:它定义一个”窗口”(一组相关行),在窗口内计算并把结果贴回每一行,行数始终不变。
据 TutorialReference 的入门示例,下面两条语句算的是同一个”全员薪资总和”,结果形态却完全不同:
-- 聚合:5 行变 1 行,明细消失
SELECT SUM(salary) AS total_salary FROM employees;
-- 窗口:5 行仍在,每行都带上总和 370000
SELECT name, department, salary,
SUM(salary) OVER() AS total_salary
FROM employees;
OVER() 是窗口函数的标志。空 OVER() 表示把整张结果集当一个窗口;加上 PARTITION BY 则把数据切成多个分区,在每个分区内独立计算。
二、OVER() 与 PARTITION BY 的拆解
OVER() 里可放两类定义:
- PARTITION BY 列:等价于
GROUP BY的”分组”逻辑,但行不折叠。窗口函数把数据分成若干”区”,在每个区内分别计算,如同每个组是一张小表。 - ORDER BY 列:决定区内行的顺序,是
ROW_NUMBER()、累计求和、滑动平均等有序计算的前提。
SELECT name, department, salary,
SUM(salary) OVER (PARTITION BY department) AS dept_total
FROM employees;
据 sqltest.online 的解析,PARTITION BY 非必填——省略时整表为一个分区,适合做全表排名或总体指标。WHERE 仍先于窗口计算执行,先过滤行再做窗口,逻辑与 GROUP BY 一致。
三、窗口函数与聚合函数的核心差异
下表综合 SegmentFault、TutorialReference、CodeGym 的对照(这是本题的核心):
| 对比维度 | 聚合函数(GROUP BY) | 窗口函数(OVER) |
|---|---|---|
| 行数变化 | 分组合并,行数减少 | 行数不变,明细全部保留 |
| 分组语法 | GROUP BY |
OVER(PARTITION BY) |
| 组内排序 | 无法直接排序 | 支持 ORDER BY 区内排序 |
| 滚动/累计计算 | 无法实现累计求和、移动平均 | 支持 ROWS/RANGE 滚动窗口 |
| 排名能力 | 无 | 专用 ROW_NUMBER/RANK/DENSE_RANK |
| 与明细列共存 | 仅分组列可共存 | 始终可与任意明细列共存 |
| 使用位置 | SELECT/HAVING |
仅 SELECT 列表 |
SegmentFault 用一个 sales 表直观展示:聚合 GROUP BY dept 只剩 2 行汇总;窗口 SUM(money) OVER(PARTITION BY dept) 则 3 行明细与部门总额并存。
四、聚合函数也能当窗口函数用
多数聚合函数(SUM/AVG/MIN/MAX/COUNT)既可配 GROUP BY 也可配 OVER()——函数本身不变,行为变。据 ConversationalSQL 举例,用 MIN()/MAX() 作窗口函数,可在每行带上”同作者最早/最晚创作年份”:
SELECT Title, Artist, YearCreated,
MIN(YearCreated) OVER (PARTITION BY Artist) AS EarliestYear,
MAX(YearCreated) OVER (PARTITION BY Artist) AS LatestYear
FROM Gallery.Paintings;
每行保留自身年份,同时携带该作者的区间极值,无需额外子查询或连接。
五、窗口函数独有:排名与框架
聚合函数做不到的两类能力,是窗口函数的价值高地:
排名函数。ROW_NUMBER()、RANK()、DENSE_RANK()、NTILE() 只在窗口语义下存在,用来在区内给行编号或分桶。CodeGym 展示了一行内同时算”区域总额”与”区域销售排名”:
SELECT region, city, sales,
SUM(sales) OVER (PARTITION BY region) AS total_sales,
RANK() OVER (PARTITION BY region ORDER BY sales DESC) AS sales_rank
FROM sales_data;
窗口框架(Window Frame)。通过 ROWS/RANGE 定义窗口内”当前行的前后范围”,实现累计求和、移动平均。没有 ORDER BY 时窗口默认覆盖整个分区;有 ORDER BY 时默认框架为”从分区首行到当前行”,这正是累计求和的实现基础。
六、选型:何时用窗口、何时用聚合
据 CodeGym 与 sqltest.online 的总结,按意图二选一:
- 用 GROUP BY/聚合:目标是生成最终汇总报表(月度总额、分类订单数),要的是更少的行。
- 用窗口函数:要保留全部明细还要附加分析(每行占比、组内排名、累计值),或需排名与滚动计算。
一个判断口诀来自 ConversationalSQL:”我既想要这个总数,又不想丢掉行”——这就是窗口函数的场景。
常见问题(FAQ)
窗口函数一定要写 PARTITION BY 吗?
不必须。省略时整表为一个分区,常用于全表排名或总体指标。
窗口函数能写在 WHERE 里吗?
不能,窗口函数只能出现在 SELECT 列表(及少数 ORDER BY);要按窗口结果过滤,需用子查询或 CTE 包一层。
聚合函数和窗口函数哪个更快?
窗口函数通常多扫一次数据且不折叠,开销略高;但免去自连接/子查询时往往更优,应以 EXPLAIN 为准。