☰
SQL窗口函数详解:OVER、PARTITION BY与排名聚合实战
2026/11/4 9:58:00 网站建设 项目流程

简介:《SQL窗口函数速查表.pdf》面向数据库管理员、数据分析师、数据科学家及软件开发人员,聚焦复杂数据集下的高效查询、趋势分析与报表生成需求,帮助读者系统梳理窗口函数的核心概念、语法结构与典型用法。资源包共1个PDF文件,大小约841KB,内容按排名函数、行序号函数和聚合函数分类编排,涵盖ROW_NUMBER()、RANK()、DENSE_RANK()以及LEAD()、LAG()等常用函数,并配示例代码说明返回值、参数含义与使用场景。速查表还归纳PARTITION BY、ORDER BY、窗口框架等语法要点,强调窗口函数保留原始行明细、可与聚合结果并列返回的特点,便于在数据探索、异常值发现、动态报表和教学学习中快速查阅。目前已有255人学习,简洁版式便于记忆与定位知识点,可作为编写高效SQL查询时的案头参考,并兼顾面试复习与日常查询场景。

1. 到底什么是 SQL 窗口函数:一个「本行小计」的类比

很多人第一次碰到 SQL 窗口函数,反应都差不多:名字听着玄乎,写法长得像聚合函数,可跑完一看,结果集的行数一行没少。打个比方,它就像超市小票上的「本行小计」——每一件商品仍然占自己那一行,旁边多出一列,告诉你这一单的合计是多少。GROUP BY 是把商品合并成一笔总数,明细直接消失;窗口函数则是开着窗户算,原表几行进来,几行出去,只是每行多带了「它所在那一组」的统计结果。值得花十分钟搞懂它,是因为同一份数据里同时看明细、看组内占比、看排名、看环比,过去要写子查询、临时表、自连接,现在一个 OVER 括号就能交代清楚,代码短了,出错的地方也少了。

2. 窗口函数为什么存在:分组汇总不丢明细的那点事

2.1 从 GROUP BY 的「合并」说起

先看一段谁都会写的查询:按部门统计销售额。

-- 每个部门一行,明细行被压掉了 SELECT dept, SUM(amount) AS dept_total FROM sales GROUP BY dept;

这段代码没问题,问题是它把「合并」这一步做绝了。原始表里三十条销售记录,查完只剩四个部门四行,张三卖了多少、李四卖了多少,全没了。可现实里的报表经常是既要又要:既要每笔订单的金额,又要这笔订单占本部门总额的比例。用 GROUP BY 走这条路,只能拿汇总结果再 join 回原表,或者写一个相关子查询。

-- 老办法:子查询 join 回原表,能跑,但啰嗦 SELECT s.emp, s.dept, s.amount, t.dept_total FROM sales s JOIN ( SELECT dept, SUM(amount) AS dept_total FROM sales GROUP BY dept ) t ON s.dept = t.dept;

这段代码的问题不在于结果错,而在于成本高、容易翻车。同一张表被扫了两遍;一旦关联键没想清楚,join 出来行数翻倍;报表里再加一个「占全公司比例」,又得再套一层子查询。窗口函数就是冲着这个痛点来的:它让聚合计算发生在每一行旁边,而不是把行合并掉。

-- 窗口函数:行数不变,每行多一列部门总额 SELECT emp, dept, amount, SUM(amount) OVER (PARTITION BY dept) AS dept_total, ROUND(amount * 1.0 / SUM(amount) OVER (PARTITION BY dept), 4) AS ratio FROM sales;

逻辑上只有一件事:SUM(amount) OVER (PARTITION BY dept)告诉数据库,「把结果按 dept 分成几个圈,在每个圈里求和,然后把和写回这个圈里的每一行」。参数上,PARTITION BY dept是分圈依据,SUM(amount)是被计算的值。没有 PARTITION BY,就是全表一个圈;加了它,就是按部门各自成圈。分母那行的OVER写法完全一样,所以占比一列顺手就出来了。

2.2 OVER 括号到底在划什么圈

OVER是窗口函数的关键字,它后面那对括号就是「划圈」的地方。括号里什么都不写,圈就是整个结果集。

SELECT emp, amount, SUM(amount) OVER () AS grand_total, -- 全表一个圈 SUM(amount) OVER (PARTITION BY dept) AS dept_total -- 每个部门一个圈 FROM sales;

这段代码能直观看出「圈」的区别:grand_total那列每一行都是同一个数,因为圈只有一个;dept_total那列在同一个部门内相同,换部门就变。理解这一点很关键——窗口函数的计算结果依赖于「当前行属于哪个圈」,而这个圈是在查询执行到 SELECT 阶段时临时算出来的,不改变底层数据的组织方式。

用教室点名来类比更顺:老师手里有全班的座位表(原始行),点名时每个人报自己的名字(明细列),同时老师还会说「你所在小组一共几个人、平均分多少」(窗口列)。座位表没有被撕掉重排,只是每个人多知道了一条组内信息。

2.3 PARTITION BY 与 ORDER BY 的分工

OVER括号里最常见的两个成分是PARTITION BY和ORDER BY,它们分工很明确:前者决定「在哪儿切圈」,后者决定「圈内按什么顺序走」。

-- 按部门分圈,圈内按月份排序,做累计求和 SELECT dept, month, amount, SUM(amount) OVER ( PARTITION BY dept ORDER BY month ) AS running_total FROM sales;

这段代码里,PARTITION BY dept让每个部门独立计算,ORDER BY month让圈内的行按月份排好队。默认情况下,一旦写了ORDER BY,窗口的帧范围就变成「从圈内第一行到当前行」,所以running_total得到的是逐月累加的金额。这是窗口函数最常用的形态之一,也是很多人第一次感觉「这东西真香」的地方。

需要留意的是这个默认行为在不同实现里存在差异,尤其是没有显式写帧范围的时候。稳妥的写法是把范围写清楚,哪怕多敲几个字:

SUM(amount) OVER ( PARTITION BY dept ORDER BY month ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS running_total

ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW的意思是「从圈内第一行开始,一直到当前这一行」,语义毫不含糊。至于帧范围还有哪些写法、ROWS 和 RANGE 差在哪,留到第 4 章和第 5 章细说。

顺带说清楚一个常见困惑:窗口里的ORDER BY和语句最后那个ORDER BY完全不是一回事。前者只影响窗口内计算的顺序,不保证最终输出顺序;后者才决定结果集怎么排。两个都写、顺序不一样,是完全可以的,也是很常见的写法。

3. 三类窗口函数各管什么:排名、取前后行、窗口内聚合

3.1 排名三兄弟:ROW_NUMBER、RANK、DENSE_RANK

排名函数解决的是「在当前圈里,这行排第几」。三个函数看起来只差几个字母,结果能差出好几行。

-- 同一批成绩,三种排名方式并排看 SELECT name, score, ROW_NUMBER() OVER (ORDER BY score DESC) AS rn, RANK() OVER (ORDER BY score DESC) AS rk, DENSE_RANK() OVER (ORDER BY score DESC) AS drk FROM exam;

跑出来的结果长这样:

namescorernrkdrk
A98111
B95222
C95322
D90443

ROW_NUMBER()是流水号,不管分数是否相同,一律按顺序发号码,所以两个 95 分拿到了 2 和 3。RANK()是「运动会式排名」:并列第二之后直接跳到第四,中间那个第三名被让出去了。DENSE_RANK()是「密集排名」:并列第二之后紧接着还是第三,号码不跳。

参数上,三个函数都不接受参数,括号里必须是空的,排序规则全部写在OVER里。这一点新手常写错,写成RANK(score)就会报错。用法上的取舍也简单:要「每人一个不重复序号」用 ROW_NUMBER,要「真实名次、允许并列跳号」用 RANK,要「名次连续、不为并列留空」用 DENSE_RANK。

3.2 取前后行:LEAD 与 LAG

有一类需求特别常见:算环比、算同比、看下一次登录时间跟这次隔了多久。这类需求的核心是「把上一行或下一行的值搬到当前行来」,LAG和LEAD就是干这个的。

-- 把上一月和下一月的金额搬到当前行,顺便算环比差值 SELECT month, amount, LAG(amount, 1, 0) OVER (ORDER BY month) AS prev_amount, LEAD(amount, 1, 0) OVER (ORDER BY month) AS next_amount, amount - LAG(amount, 1) OVER (ORDER BY month) AS diff FROM monthly_sales;

参数得说清楚:第一个参数是要取的列,第二个参数是偏移量(往前或往后数几行,不写默认 1),第三个参数是取不到时的默认值(不写就返回 NULL)。上面LAG(amount, 1, 0)的意思是「取上一行的 amount,如果当前是第一行没有上一行,就给 0」。第三行的LAG(amount, 1)没给默认值,第一行的diff就会是 NULL,做减法时结果自然也是 NULL。

如果想让结果干净一点,可以在外面套一层处理 NULL 的函数,或者在业务侧决定 NULL 代表什么。这里有个容易踩的坑:默认值给 0 在某些场景下是错的,比如算增长率时把「没有上期」当成「上期是 0」,除下去就炸了。默认值给什么,取决于这个 NULL 在业务上意味着什么,不能图省事一律填 0。

3.3 窗口内聚合:SUM、AVG、COUNT、MIN、MAX

第三类最没有门槛,就是把熟悉的聚合函数加上OVER。函数还是那几个函数,只是不再合并行。

-- 部门内求和、求平均、算占比、找最大单 SELECT dept, emp, amount, SUM(amount) OVER (PARTITION BY dept) AS dept_total, AVG(amount) OVER (PARTITION BY dept) AS dept_avg, COUNT(*) OVER (PARTITION BY dept) AS dept_cnt, MAX(amount) OVER (PARTITION BY dept) AS dept_max, ROUND(amount * 1.0 / SUM(amount) OVER (PARTITION BY dept), 4) AS ratio FROM sales;

这段代码用一次扫描,把「部门总额、部门均值、部门人数、部门最大单、本人占部门比例」五件事一起算完了。换成 GROUP BY 的写法,至少两三次查询或者多层子查询起步。这也是窗口函数在报表场景里最实在的价值:它把多次扫描压成一次。

三类函数的分工可以简单记成一张表:

类别代表函数回答的问题典型场景
排名函数ROW_NUMBER、RANK、DENSE_RANK这行在圈里排第几取每组前 N 名、去重取最新
行序号函数LEAD、LAG上一行、下一行是什么环比、同比、间隔时长
窗口内聚合SUM、AVG、COUNT、MIN、MAX这行所在圈的总量是多少占比、累计、与组均值比较

4. OVER 括号里怎么搭:语法结构与帧范围的完整拆解

4.1 基本语法骨架

把窗口函数拆开,骨架其实只有两层:函数部分和窗口部分。

-- 通用骨架:函数 + OVER(分圈 + 排序 + 帧范围) function_name([DISTINCT | ALL] expression) OVER ( [PARTITION BY partition_expression, ...] [ORDER BY sort_expression [ASC | DESC], ...] [frame_clause] )

function_name是具体函数名,expression是被计算的列或表达式。DISTINCT和ALL用来声明是否去重,ALL是默认行为,可以不写;DISTINCT在窗口函数里的支持程度因实现而异,用之前最好确认一下当前环境是否认这个写法,别写完跑不通还以为是语法记错了。

PARTITION BY后面可以跟多个列,表示按组合分圈,比如PARTITION BY dept, region就是「部门 + 区域」共同决定一个圈。ORDER BY同样可以多列,还可以各自指定升降序。这三段都是可选的,最简形态OVER ()也完全合法,只是圈变成了全集。

真正容易出错的是第三段——帧范围。它决定「圈里的哪些行参与这一行的计算」,是窗口函数里最容易写得含糊的部分。

4.2 帧范围:ROWS 与 RANGE 到底选哪个

帧范围的常见写法有这么几种组合:

-- 累计:从圈内第一行到当前行 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW -- 移动平均:当前行及之前两行,共三行 ROWS BETWEEN 2 PRECEDING AND CURRENT ROW -- 前后各一行,共三行 ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING -- 从当前行到圈内最后一行 ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING

关键字含义很直白:UNBOUNDED PRECEDING是圈的开头,UNBOUNDED FOLLOWING是圈的结尾,CURRENT ROW是当前行,n PRECEDING和n FOLLOWING是往前数 n 行、往后数 n 行。写移动平均时最常用的就是n PRECEDING AND CURRENT ROW这种形态。

-- 三个月移动平均:当前月加上前两个月 SELECT month, amount, AVG(amount) OVER ( ORDER BY month ROWS BETWEEN 2 PRECEDING AND CURRENT ROW ) AS ma_3 FROM monthly_sales;

这里AVG的分母不是固定 3,而是「当前行往前能凑到几行就算几行」。第一个月只有自己一行,均值就是它本身;第二个月有两行,均值是这两行的平均。如果希望前期也按三个月算,就得在业务侧决定要不要补零,窗口函数本身不会替你补。

ROWS和RANGE的差别只在遇到并列值时暴露出来。ROWS数的是物理行,「前两行」就是实打实的两条记录;RANGE看的是排序值,「前两行」会变成「排序值落在某个区间内的所有行」,同值的行会被一起拉进来。平时用ROWS更可控,尤其在数据有并列值、又想精确控制行数的时候。

关键字计数依据遇到并列值时适合的场景
ROWS物理行数按行数精确取,不理会值是否相同移动平均、固定窗口滑窗
RANGE排序值区间同值行一起纳入计算按时间区间聚合、按分值段统计

4.3 窗口名复用:把长 OVER 写短

一个查询里如果多个窗口列用完全相同的分圈和排序规则,重复写OVER (...)会很长。标准里提供了WINDOW子句,可以给窗口起个名字复用。

-- 先定义窗口 w,SELECT 里直接引用 SELECT dept, month, amount, SUM(amount) OVER w AS running_total, AVG(amount) OVER w AS running_avg FROM sales WINDOW w AS ( PARTITION BY dept ORDER BY month ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW );

这段写法的好处是「窗口定义只出现一次」,改分圈规则时改一个地方就够了。要注意WINDOW子句的位置在FROM和WHERE之后、ORDER BY之前,写错位置语法就过不去。另外,并非所有实现都支持WINDOW子句,遇到不支持的环境,老老实实把OVER (...)重复写两遍也能跑,只是可读性差一点。用之前确认一下当前环境,比写完报错再回头改省事。

5. 五个最容易搞混的地方:窗口函数常见误区与区别

5.1 窗口函数不等于分组汇总

易混点:两者都能对一组行做 SUM、AVG、COUNT,写出来长得也像,很容易以为窗口函数是 GROUP BY 的「新写法」。

为什么混:SUM(amount)这个表达式在两边都出现,初学者看到熟悉的函数名就默认行为一样。实际上一个改变行数,一个不改变行数,这是本质差别。

怎么区分:看结果行数。GROUP BY dept出来是每个部门一行;SUM(amount) OVER (PARTITION BY dept)出来还是原来的行数。再看能不能在 SELECT 里同时选非分组列——GROUP BY 下选了emp却不放进分组,很多实现会直接报错;窗口函数没有这个限制,因为每一行本来就还在。还有一点:窗口函数的计算发生在 SELECT 阶段,所以它的结果不能直接用在 WHERE 里筛,这一点下一条细说。

5.2 WHERE 和 HAVING 里用不了窗口函数

易混点:想筛出「排名前三」,顺手写成WHERE ROW_NUMBER() OVER (...) <= 3,然后被报错拦下来。

为什么混:直觉上觉得窗口列跟普通列一样,算出来就能筛。但 SQL 的执行顺序是先 FROM、WHERE,再 GROUP BY、HAVING,最后才轮到 SELECT。窗口函数是在 SELECT 阶段才求值的,等它算出来,WHERE 早就执行完了,自然拿不到这个值。这也解释了一个常见的「黑匣子」感受:明明写对了,数据库就是不认。

怎么区分:凡是要对窗口结果做过滤、排序、再计算的,一律先把它放进子查询或 CTE,外层再筛。

-- 错误写法:WHERE 里直接用窗口函数,会报错 SELECT * FROM sales WHERE ROW_NUMBER() OVER (ORDER BY amount DESC) <= 3; -- 正确写法:内层算序号,外层过滤 SELECT * FROM ( SELECT s.*, ROW_NUMBER() OVER (ORDER BY amount DESC) AS rn FROM sales s ) t WHERE rn <= 3;

5.3 取每组前三名时,ROW_NUMBER 和 RANK 结果不一样

易混点:需求是「每个部门取金额最高的三笔」,用 ROW_NUMBER 和 RANK 都能写出<= 3的过滤,看起来只是名字不同。

为什么混:两个函数都返回数字,排序规则也一样,不看数据很难发现差异。差异只在有并列值时出现。

怎么区分:想清楚「并列的第三名要不要都留下」。如果部门里第三、第四笔金额相同,用 ROW_NUMBER 只会留下其中一个,另一个被序号挤到第四位;用 RANK 则两个都是第三名,都会被留下,返回行数可能超过三行。业务上「取三条记录」和「取成绩前三的人」是两回事,前者用 ROW_NUMBER 保证数量,后者用 RANK 保证名次含义。这一点在报表里最容易出错,因为样本数据往往没有并列值,上线后遇到真实数据才发现结果忽多忽少。

5.4 ROWS 和 RANGE 遇到并列值会算岔

易混点:ROWS BETWEEN 1 PRECEDING AND CURRENT ROW和RANGE BETWEEN 1 PRECEDING AND CURRENT ROW只差一个词,很多人默认它们等价。

为什么混:在没有并列值、排序值又连续的情况下,两者结果确实一样,跑几次都不会出问题,于是就被当成同义词记下来了。

怎么区分:造一组带并列值的数据,比如两个人都是 95 分,看累计求和的结果。ROWS严格按行推进,第二个人累加时只加上第一个人;RANGE会把「排序值相同的行」当成一个整体,同分的行互相能看到对方的结果。做累计、做移动平均时,如果不确定数据有没有并列值,优先写ROWS,语义清楚、结果可预期。另外,写ORDER BY却不写帧范围时,默认帧的行为在不同实现之间也有出入,稳妥的做法永远是把帧范围显式写出来。

5.5 窗口里的 ORDER BY 不等于最后的 ORDER BY

易混点:看到OVER (ORDER BY month)就以为结果集会按月排好。

为什么混:ORDER BY这个词太熟悉,出现在哪都以为是在排序输出。

怎么区分:窗口里的ORDER BY只服务于「圈内按什么顺序计算」,比如累计求和按月份推进、排名按分数从高到低。它不承诺最终输出的行序。想要结果按月份排,语句末尾还得单独写一个ORDER BY month。两个都写、写的不一样,语法上完全合法,结果也符合各自的作用。刚上手时容易只写窗口里那个,然后纳闷为什么输出顺序看着随机——这不是数据库的玄学,是两件事被当成了一件。

6. 什么时候该用窗口函数,什么时候别硬上

判断标准其实不难,抓住一条就行:你需要在保留明细行的同时,得到「它所在那一组」的信息。符合这条,窗口函数基本就是最优解;不符合,硬套反而会让查询变慢、变难读。

下面这几种情况,用窗口函数是顺手的:

需求推荐做法原因
每行占比、组内排名窗口函数一次扫描同时拿到明细和组内信息
每组取最新一条记录ROW_NUMBER 加外层过滤不用自连接,逻辑集中
环比、同比、间隔天数LAG / LEAD把前一行搬到当前行,写法直接
累计求和、移动平均窗口聚合加帧范围帧范围一写,语义明确
只要每组的汇总值GROUP BY不需要明细,窗口函数纯属浪费

最后一行值得展开。如果报表只要「每个部门总额」这一列,用GROUP BY dept就够了,结果集更小,优化器也更容易走索引。用SUM(amount) OVER (PARTITION BY dept)再套一层DISTINCT去重,属于把简单问题绕成复杂问题,性能还未必好。窗口函数不是「更高级的 GROUP BY」,它是解决另一类问题的工具。

反过来,有些场景看着能用,实际上代价不小。比如在大表上对高基数列做PARTITION BY,等于先做一次大规模分组,内存和临时空间都会被吃掉;再比如窗口函数套窗口函数,虽然部分实现允许在外层再开窗,但可读性会迅速下降,这时候拆成两步、中间落一张临时表往往更省心。

写窗口函数时,我自己的习惯是固定三步。第一步,先把PARTITION BY写出来,问自己「这个数是在哪个范围内算的」,圈划错了后面全错。第二步,把帧范围显式写出来,哪怕只是最简单的累计,也不依赖默认行为。第三步,如果结果还要过滤,直接套一层子查询,别想着在 WHERE 里硬来。

举一个完整的小例子,把三步走一遍:要查每个部门金额最高的两个人,并显示他们占部门总额的比例。

-- 内层:分圈排名 + 分圈求和;外层:按名次过滤并按比例排序 SELECT dept, emp, amount, dept_total, ratio FROM ( SELECT dept, emp, amount, SUM(amount) OVER (PARTITION BY dept) AS dept_total, ROW_NUMBER() OVER ( PARTITION BY dept ORDER BY amount DESC ) AS rn FROM sales ) t WHERE rn <= 2 ORDER BY dept, ratio DESC;

内层的两个窗口用的是同一个分圈规则,一个算总额、一个发序号;ORDER BY amount DESC决定谁排第一;外层用rn <= 2做过滤,最后再按部门和占比排序。参数上唯一需要斟酌的是rn用 ROW_NUMBER 还是 RANK——如果金额有并列、又希望并列的都留下,换成RANK()即可,代价是返回行数可能超过两行。

有一回我在报表里图省事,把「每组取最新一条」写成了RANK()加<= 1,样本数据里每条时间都不同,跑得好好的。上线后数据里出现了同一秒的两条记录,结果每个这样的组都返回两行,下游汇总直接翻倍。从那以后我每次写窗口函数,都强制先把分圈、排序、并列值处理这三件事在纸上过一遍,再动手敲 SQL。这类问题的后悔药不好买,提前想清楚最省事。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询