☰
SQL窗口函数详解:组内排名、累计求和与移动平均
2026/10/27 2:59:38 网站建设 项目流程

简介:《SQL窗口函数速查表》是一份面向数据库管理员、数据分析师、数据科学家与开发人员的PDF速查资料,聚焦窗口函数从概念、语法到实战应用的系统梳理,适合希望提升复杂查询与报表分析效率的初中级及进阶技术人员。整包仅含1个PDF文件,约841KB,便于随身查阅和快速检索。目前已有255人学习下载。内容按功能与用途组织,覆盖ROW_NUMBER、RANK、DENSE_RANK、LEAD、LAG以及SUM、AVG、COUNT、MIN、MAX等窗口聚合,说明OVER、PARTITION BY、ORDER BY、frame_clause等语法要点,并配有示例代码和应用场景,可帮助读者快速定位函数、理解返回结果,并在数据探索、报表生成和应用程序查询构建中查漏补缺。

1. 到底什么是SQL窗口函数:一次算出一组的结果,却不合并这些行

第一次看到ROW_NUMBER() OVER (PARTITION BY 部门 ORDER BY 工资 DESC)这种写法的人,多半会愣一下:括号套括号,还冒出个OVER,看着像某种玄学咒语。把它翻译成大白话其实只有一句:先按某个规则把表分成几桌,每桌内部排个序、算笔账,再把算出来的结果贴回原来那一行,一行都不删。这就是 SQL 窗口函数——窗口,指的就是“当前这一行能看见的那一批行”。

它值得搞懂,是因为很多在 Excel 里靠拖拽填充、在程序里靠循环才能做出来的事——组内排名、环比增长、累计求和、取上一行对比——用一段 SQL 就能一次算完。学会它,等于把“取数”和“算数”两步并成了一步。

2. 为什么要有窗口函数:GROUP BY 算得对,却把细节弄丢了

2.1 用 GROUP BY 做组内排名,问题出在哪

先看一个再普通不过的需求:一张员工表,想给每个部门内部按工资从高到低排个名次。

用传统思路,第一反应是GROUP BY。可一写就发现不对劲——GROUP BY 部门之后,每个部门只剩一行了,那张三、李四、王五的工资明细去哪了?都被“压”成了AVG(工资)、MAX(工资)这样的一个数。想排名次,就得先聚合再关联回原表,或者干脆把数据拉到程序里循环。

问题的根子在于:聚合函数的职责是“多行变一行”,它天生就要消灭明细。而“给每一行标个名次”这个需求,恰恰要求明细一行都不能少——一共 20 个员工,就得返回 20 行,只是每行多带一列“你在你们部门排第几”。

这就是窗口函数存在的理由。它借用聚合的算法,却拒绝聚合的“合并”动作:一组 20 行进去,还是 20 行出来,但每行都额外拿到了这一组的统计信息。打个比方,GROUP BY像班长收走全班的作业本,最后只交上来一张平均分表;窗口函数则是班长当着你面翻完所有人的分数,然后把“你排第几、班级平均分多少”写在你的本子上,本子还还给你。

2.2 OVER 到底改变了什么

所有窗口函数的标志都是那个OVER关键字。可以把它理解成一个“取景框”:

-- 没有 OVER:这是普通聚合,多行压成一行 SELECT 部门, AVG(工资) FROM 员工表 GROUP BY 部门; -- 有 OVER:每一行都保留,并在旁边贴一个“本部门平均工资” SELECT 姓名, 部门, 工资, AVG(工资) OVER (PARTITION BY 部门) AS 部门平均工资 -- 取景框:本部门全体 FROM 员工表;

上面第二条语句返回的行数和原表完全一致。第 4 列对同一个部门的每个人来说都是同一个值,因为大家的“取景框”是同一批行。换个说法:OVER后面那对括号,定义的是“我这一行往外看,能看见谁”。看不见的行,不参与计算,也不会让我的这一行消失。

这个“看得见”的范围,就是窗口。窗口可大可小:可以是整张表,可以是同部门的几行,也可以是“从第一行到当前行”这样一个不断变长的区间。窗口函数的全部威力,都藏在这个范围的定义里。

2.3 一个销售额的例子:两种写法差在哪

假设有一张月度销售表,字段是月份、销售额,想同时看到三样东西:当月销售额、全年累计到当月的总额、以及当月占全年的比例。

用GROUP BY的思路,你得先算总和,再JOIN回原表,写起来是一堆子查询。用窗口函数,一次成型:

SELECT 月份, 销售额, -- 窗口:整张表,从第一行累计到当前行 SUM(销售额) OVER (ORDER BY 月份 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS 累计销售额, -- 窗口:整张表全体,不带 ORDER BY 就是全表求和 SUM(销售额) OVER () AS 全年总额 FROM 月度销售表;

这里有两个细节值得留意。第一,SUM(...) OVER ()括号里什么都不写,代表整张表就是一个窗口,所以每一行拿到的“全年总额”都相同,但它不会把 12 行压成 1 行。第二,ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW把窗口缩成了“从头到我这一行”,于是每一行的累计值都不同,形成了一条累加曲线。

同样一个SUM,加上OVER就是窗口函数,不加就是聚合函数。语法只差这一个词,语义却从“压缩”变成了“并排”。理解这一点,窗口函数就已经懂了一半。

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

按用途分,常用的窗口函数大致三族。分类不是为了背,而是为了在拿到需求时能立刻反应出“这题该找谁”。

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

这三个函数长得极像,输出的却是不一样的名次表。用一组成绩来说明:

姓名分数ROW_NUMBERRANKDENSE_RANK
A95111
B90222
C90322
D85443
  • ROW_NUMBER():不管分数是否相同,按顺序硬编号,1、2、3、4,绝不重复。它回答的是“你是第几个被我点到的”。
  • RANK():并列同名次,但会留下空档。两个 90 分并列第 2 之后,下一个直接跳到第 4。它回答的是“排在你前面的人有几个,再加一”。
  • DENSE_RANK():并列同名次,且不留空档,第 3 名紧跟着第 2 名。它回答的是“你属于第几档”。

什么时候用哪个,取决于业务语义。要“取每个部门工资最高的那一个人”,用ROW_NUMBER最稳,因为它保证只返回一行,不会因为并列而多出人来。要“评奖学金等级”,可能DENSE_RANK更贴合,因为名次里不该有空洞。要“看看有多少人排在我前面”,RANK才对。这三个的差别不是谁更高级,而是对“并列”这件事的处理态度不同。

写法上它们都需要ORDER BY,否则排名毫无意义:

SELECT 姓名, 部门, 工资, ROW_NUMBER() OVER (PARTITION BY 部门 ORDER BY 工资 DESC) AS 部门内序号, RANK() OVER (PARTITION BY 部门 ORDER BY 工资 DESC) AS 部门内名次, DENSE_RANK() OVER (PARTITION BY 部门 ORDER BY 工资 DESC) AS 部门内档次 FROM 员工表;

注意ORDER BY 工资 DESC里的DESC——排名默认是升序,工资排名当然要倒着来。这个小小的方向搞反,结果就全反了,是个很常见的翻车点。

3.2 取前后行:LEAD 与 LAG,把“上一期”搬到同一行

LEAD()和LAG()解决的是另一类痛苦:比较相邻两行。比如算环比增长,你需要“本月销售额”和“上月销售额”出现在同一行里才能相减。传统做法是自关联,写起来一大坨,还容易因为月份缺失而对不上。

SELECT 月份, 销售额, LAG(销售额, 1) OVER (ORDER BY 月份) AS 上月销售额, -- 往前看 1 行 LEAD(销售额, 1) OVER (ORDER BY 月份) AS 下月销售额, -- 往后看 1 行 销售额 - LAG(销售额, 1) OVER (ORDER BY 月份) AS 环比增量 FROM 月度销售表;

两个函数的完整签名是LAG(表达式, 偏移量, 默认值)。第二个参数不写就是 1,第三个参数用于处理“前面没行了”的情况——比如第一月的上月销售额,默认返回NULL,可以指定成 0。这个第三参数在实际报表里非常实用,能省掉一层COALESCE。

LAG和LEAD的价值在于:它把“跨行比较”这件事变成了“同行相减”。一旦上一期的值被搬到当前行,后面所有的加减乘除、比率计算都变成了普通的列运算,逻辑一下子清爽了。

3.3 窗口内聚合:SUM、AVG、COUNT 的滑动用法

第三族是大家最熟的聚合函数,只是加了OVER。它们能做三类事:全窗口聚合(整表或整组的汇总贴在每行旁边)、累计聚合(从开头累加到当前行)、滑动聚合(固定长度的移动窗口)。

SELECT 日期, 销售额, -- 累计:从第一天累加到今天 SUM(销售额) OVER (ORDER BY 日期 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS 累计销售额, -- 滑动:最近 7 行(含当前行)的移动平均 AVG(销售额) OVER (ORDER BY 日期 ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS 七日移动平均, -- 全组:本部门总人数,每行都相同 COUNT(*) OVER (PARTITION BY 部门) AS 部门人数 FROM 日销售表;

移动平均那段值得多看一眼:ROWS BETWEEN 6 PRECEDING AND CURRENT ROW表示窗口是“当前行往前数 6 行,加上当前行”,一共 7 行。所以想要 7 日均线,写的是 6 而不是 7——这是初学者最容易写错的一个数。记住“N 日均线写 N-1”,能省下不少调试时间。

COUNT(*) OVER (PARTITION BY 部门)这种写法还常被用来做“占比”题:先算出每组的行数,再让每行的值除以它,就得到组内占比,全程不需要子查询。

4. 窗口的三个零件:PARTITION BY、ORDER BY 和帧范围

OVER括号里可以塞三样东西,它们共同决定“这一行能看见谁”。三者的关系,可以用一个类比串起来:把整张表想成一个大操场,PARTITION BY是把人分成几个班,ORDER BY是每个班内按身高排队,帧范围则是“从队首到你现在站的位置”这段队伍。

4.1 PARTITION BY:把一张大表切成互不干扰的小桌

PARTITION BY 列名的作用是分组,但它和GROUP BY有本质区别:GROUP BY分组之后每组只剩一行,PARTITION BY分组之后行数不变,只是每组各算各的。

SELECT 姓名, 部门, 工资, -- 每个部门单独算平均,互不干扰 AVG(工资) OVER (PARTITION BY 部门) AS 部门平均, -- 不写 PARTITION BY,整张表算一个窗口 AVG(工资) OVER () AS 全公司平均 FROM 员工表;

不写PARTITION BY时,整张表就是唯一的一个窗口,所有行共享同一个计算结果。这一点在算“全公司占比”时特别方便:分子用带分组的表达式,分母用不带分组的表达式,一个查询里两种粒度并存,这是GROUP BY做不到的。

PARTITION BY后面可以跟多个列,用逗号隔开,表示按组合分组,比如PARTITION BY 年份, 部门。但要注意,PARTITION BY里不能写聚合函数的嵌套,也不能引用别名(取决于具体数据库的宽松程度),稳妥起见直接写原始列名。

4.2 ORDER BY:决定“前后”和“累计”的方向

PARTITION BY决定“和谁一组”,ORDER BY决定“组内怎么排”。凡是涉及“累计”“前后行”“名次”的窗口函数,都离不开它。

ORDER BY在窗口里有两个作用,很多人只记住了第一个:

  1. 决定排名和取前后行的顺序——RANK()、LAG()必须有它,否则顺序不确定。
  2. 决定累计类聚合的默认窗口范围——一旦写了ORDER BY,SUM(...) OVER (ORDER BY ...)的默认窗口就从“整组”变成了“从组首到当前行”。这个默认行为是 SQL 标准里的规定,也是无数人“算出来数不对”的根源。

第二点值得单独强调。看下面两行,只差一个ORDER BY,结果完全不同:

-- 没有 ORDER BY:整组求和,每行都是同一个总数 SUM(金额) OVER (PARTITION BY 客户) AS 客户总额 -- 有 ORDER BY:默认变成累计求和,每行的值都不一样 SUM(金额) OVER (PARTITION BY 客户 ORDER BY 下单时间) AS 累计消费

如果你本意是要客户总额,却顺手写了ORDER BY,得到的就是一条不断累加的曲线,而不是一个固定值。这类问题不会报错,只会静悄悄地给出错误答案,属于典型的“黑匣子”式困惑——结果看着像对的,其实全错。

4.3 帧范围:ROWS 与 RANGE 的差别,以及那个容易被忽略的默认值

帧范围(frame clause)是三个零件里最陌生、也最容易忽略的一个。它写在ORDER BY之后,形如ROWS BETWEEN ... AND ...或RANGE BETWEEN ... AND ...,用来精确划定“从哪一行到哪一行”。

常见写法有这几种:

帧写法含义
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW从组首到当前行,逐行累加
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW当前行及往前 6 行,共 7 行
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING前后各一行,共 3 行
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING整组,等同于不写 ORDER BY 的效果

ROWS和RANGE的区别在于计数方式:ROWS按“物理行数”数,RANGE按“排序列的值”算。举个例子,如果排序列上有三个值相同的行,ROWS BETWEEN 1 PRECEDING AND CURRENT ROW只会包含当前行和它上面的一行;而RANGE BETWEEN 1 PRECEDING AND CURRENT ROW会把所有值相等的“并列行”一起纳入。日常做累计和移动平均,ROWS更符合直觉,也更常用;RANGE更适合处理“按数值区间取窗口”的场景。

提示:不写帧范围时,只要带了ORDER BY,默认帧就是RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW。这意味着默认情况下并列行会被一起算进去。当你发现累计值在重复值那里“跳了一下”,多半就是这个默认值在起作用,显式写上ROWS就能避免。

5. 易混点辨析:五个最容易搞混的地方

窗口函数的语法不难,难的是几个长得像、语义却相反的概念。下面五组,是初学者最常混的地方。

5.1 易混点一:窗口函数和 GROUP BY 是不是一回事

易混在哪:两者都涉及“分组”和“聚合”,写出来的 SQL 里都有PARTITION BY或GROUP BY这样的分组关键字,结果里也都能出现平均值、总和。

为什么混:因为窗口函数确实复用了聚合算法,SUM、AVG、COUNT这些名字完全一样,不仔细看还以为只是换了个写法。

怎么区分:看返回的行数。GROUP BY是“多行变一行”,每个组只留一条记录;窗口函数是“多行进、多行出”,行数一行不少。判断标准很干脆:如果你的结果需要保留原始明细(每笔订单、每个员工、每一天),就必须用窗口函数;如果你只要每个组的一个汇总数,GROUP BY更省资源。另外,GROUP BY后面可以用HAVING过滤组,窗口函数的结果不能用HAVING过滤。

5.2 易混点二:ROW_NUMBER 和 RANK 到底差在哪

易混在哪:数据里没有重复值时,两者输出完全一样;有重复值时,差别才暴露出来。

为什么混:名字都带“NUMBER”或“RANK”的意味,中文里又都能叫“排名”,测试时顺手拿了一组不重复的数据,结果自然看不出区别。

怎么区分:记住空档。ROW_NUMBER永远连续,1、2、3、4;RANK并列之后跳号,1、2、2、4;DENSE_RANK并列之后不跳号,1、2、2、3。如果业务要求“每个组只取一条”,用ROW_NUMBER最安全,因为它保证不重复;如果业务要求“并列的人名次要一样”,就必须用RANK或DENSE_RANK。拿ROW_NUMBER去做并列排名,会让同分的两个人莫名其妙分出先后,这是个隐蔽的错误。

5.3 易混点三:窗口函数能不能写在 WHERE 里

易混在哪:直觉上,既然窗口函数能算出一个值,那自然可以拿它来过滤——“只保留排名第一的行”听起来天经地义。

为什么混:SQL 的书写顺序和执行顺序不一致。你写的是SELECT ... WHERE ...,但数据库实际执行时,WHERE先于SELECT里的窗口计算。也就是说,WHERE运行时,窗口函数的结果还不存在,自然没法拿来比较。

怎么区分:记住执行顺序:FROM→WHERE→GROUP BY→HAVING→ 窗口函数 →ORDER BY→LIMIT。窗口函数的位置在WHERE和HAVING之后。要过滤窗口结果,必须套一层子查询或 CTE:

-- 错误写法:WHERE 里直接用窗口函数会报错 -- SELECT * FROM 员工表 WHERE ROW_NUMBER() OVER (...) = 1; -- 正确写法:先算出序号,外层再过滤 WITH 带序号 AS ( SELECT 姓名, 部门, 工资, ROW_NUMBER() OVER (PARTITION BY 部门 ORDER BY 工资 DESC) AS rn FROM 员工表 ) SELECT 姓名, 部门, 工资 FROM 带序号 WHERE rn = 1; -- 每个部门工资最高的那一位

这个“先算后筛”的两层结构,是窗口函数最常见的用法模板,几乎所有的“每组取 Top N”都长这样。

5.4 易混点四:ROWS 和 RANGE 就差一个字母,效果一样吗

易混在哪:两个词只差几个字母,语法位置相同,很多教程一带而过,让人以为可以互换。

为什么混:在数据没有重复值、排序列唯一的情况下,两者的结果确实一样,于是“反正都对”的印象就留下了。

怎么区分:看排序列有没有重复值。ROWS数的是物理行,RANGE比的是排序列的值。一旦出现并列(比如两个订单同一时间下单),ROWS BETWEEN 1 PRECEDING AND CURRENT ROW只取一行加当前行,而RANGE会把值相同的行全部拉进来,窗口一下变大。做移动平均、累计求和时,如果要的是“固定行数”的窗口,务必写ROWS;如果要的是“某个数值区间”,才用RANGE。

5.5 易混点五:窗口函数会不会把行合并,导致数据变少

易混在哪:用了SUM(...) OVER (...)之后,看到同一个部门的每行都是同一个总和,很容易以为“数据被折叠了”。

为什么混:因为每行的值重复了,视觉上像是聚合结果被复制粘贴,而不是原始明细。

怎么区分:数行数。窗口函数绝不减少行数(除非你在外层又加了DISTINCT或GROUP BY)。每行仍然对应原表的一条记录,只是多了一列“参考信息”。真正会减少行数的是GROUP BY、DISTINCT和外层的过滤条件,注意别把这几件事的责任混到一起。

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

把边界说清楚,比多背几个函数有用。

适合用的场景,有几个共同特征:既要明细又要汇总;需要组内排名或每组取前 N;需要跨行比较(环比、同比、相邻差);需要累计值或移动平均。这四类需求,用窗口函数基本都是一段 SQL 的事,而且不用把数据搬到程序里循环。

不适合用的场景也很明确:

场景为什么不建议
只要每组的汇总数,不需要明细用GROUP BY更直接,执行计划也更省
数据量极大且窗口定义复杂排序和分区开销不小,先看执行计划再决定
只需要简单的行号有些数据库有更轻量的替代方案,不必上重武器
逻辑里包含多层嵌套的窗口再过滤结构会变得很难读,考虑拆成多个 CTE

还有一个实践中的分寸:窗口函数不是越多越好。一个查询里叠五六个窗口,每个都带不同的PARTITION BY和帧范围,别人接手时基本只能靠猜。宁肯拆成几步 CTE,一步一个窗口,每一步起个说得清的名字,也不要写成一行 200 字符的表达式。可读性本身就是可维护性。

举个具体例子收尾。某次做月度报表,我需要每个客户每月的消费额、该客户的历史累计消费、以及该客户在所有客户中的当月排名。三个指标,三个不同的窗口:第一个是原始明细,第二个按客户分区加时间排序累加,第三个按月份分区按消费额排序。我当时图省事,把三个窗口塞进了一条SELECT,跑出来发现累计那一列每个月都在跳变——排查了半天才想起来,累计的窗口里忘了写ROWS,数据库用了默认的RANGE,而那个月正好有几笔金额相同的订单,被一起算了进去。

从那以后,我每次写累计或移动平均,都强制把ROWS BETWEEN ... AND ...完整写出来,哪怕是“默认值也一样”的情况——把那点不确定性提前钉死,比事后找后悔药便宜得多。窗口函数本身不难,难的是每个零件都要交代清楚,漏一个,结果就会悄悄跑偏。希望这篇能帮你在下次写OVER的时候,少走一点弯路。

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

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

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

立即咨询