☰
SQL窗口函数零基础速查:OVER、排名、累计与移动平均
2026/11/4 5:06:45 网站建设 项目流程

简介:这份《SQL窗口函数速查表》以PDF形式系统整理了SQL窗口函数的核心知识,面向数据库管理员、数据分析师、数据科学家与开发人员,尤其适合需要在复杂数据集上编写高效查询、提升数据分析效率的中级技术人员,也可作为教学与自学的参考资料。资源包共1个PDF文件,大小约841KB,属于轻量级速查文档,便于随查随用、快速检索。目前已有255人学习,具备一定的实用参考基础。内容按功能分类展开:先说明窗口函数与普通聚合函数的区别,再逐一介绍ROW_NUMBER、RANK、DENSE_RANK等排名函数,LEAD、LAG等行序号函数,以及SUM、AVG、COUNT、MIN、MAX等窗口聚合函数,并给出OVER、PARTITION BY、ORDER BY、frame子句的语法结构与参数含义。每个函数均配有示例代码,帮助理解返回值与具体用法,同时覆盖销售增长率、分组均值异常识别、实时报表等典型场景,便于读者按需定位知识点并迁移到实际查询中。

1. 先别急着背函数:窗口函数到底解决的是什么麻烦

很多人第一次听到「SQL 窗口函数」这几个字,第一反应是先把页面关掉——听起来像是数据库老手才碰的东西。它解决的麻烦其实很朴素:你手上有一张成绩单或者订单表,想在每一行旁边再加一列「本班平均分」或者「这笔订单占客户总额的百分比」,但又不想把原来的行合并掉。传统分组统计会把十行压成一行,明细就没了;窗口函数不压行,算完的结果直接贴在每一行旁边。这份速查表就是为这件事准备的,它把常用写法按用途归好类,零基础的人也能按图索骥。不管你写 SQL 是为了做报表、查数据,还是刚入职被安排跑一段查询,先把它当成一本查得到、抄得走的手册就够了。

2. 为什么值得先学窗口函数:它把「分组」和「明细」放回同一张表

大部分人学 SQL 的路线是:SELECT 查列、WHERE 筛行、GROUP BY 分组、ORDER BY 排序。走完这四步,日常取数需求能应付八成,直到某天需求变成「在每个销售人员的每一笔订单后面,标出他在本区域的排名,同时算出这笔订单占区域总额的比例」。这时候你会发现 GROUP BY 给出的答案永远差一口气:它有排名需要的总额,却没有明细所在的那几行。窗口函数就是补上这一口气的工具,而这份按用途分类的速查表,是新手把这个工具用顺手的最短路径。

2.1 先看清 GROUP BY 的天花板在哪

GROUP BY 的工作方式可以拆成三步:按指定列把行分堆,每堆算出一个汇总值,最后每堆只保留一行。行数从「多少条记录」变成「多少个分组」,明细在这个过程中被丢掉了。它本身没有问题,设计目标就是汇总。当需求既要汇总又要明细时,只靠 GROUP BY 是凑不出来的,必须在外面再套一层,把明细表按分组键拼回汇总结果。

-- GROUP BY 只看得到汇总:每个区域一行,具体是谁卖的、单笔多少钱,全没了 SELECT region, SUM(amount) AS region_total FROM sales_demo GROUP BY region;

这段查询返回两行:区域名和该区域的总额。销售人员的名字、每一笔的金额、编号,全都看不见了。如果你的报表就是「每个区域一行」,那到这里就结束了,用 GROUP BY 完全够。但只要报表里还需要出现「人」和「单」这两个维度,GROUP BY 就只完成了上半场。

2.2 传统解法:子查询拼回去,能跑但难维护

在窗口函数普及之前,标准解法是把汇总结果当成一张临时表,再 JOIN 回明细。下面这段代码做的事情是:先用子查询算出每个区域的总额,再把它按区域拼回每一笔销售记录上。

-- 传统解法:先把每组的汇总算出来,再按分组键拼回明细 SELECT s.sale_id, s.region, s.salesperson, s.amount, t.region_total, -- 区域总额,来自子查询 ROUND(s.amount / t.region_total * 100, 2) AS pct_of_region -- 该笔占区域的百分比 FROM sales_demo AS s JOIN ( SELECT region, SUM(amount) AS region_total FROM sales_demo GROUP BY region ) AS t ON t.region = s.region;

这段代码能跑出正确结果,但它有三个让新手头疼的地方。第一是结构长,一眼看不出在算什么,得从里往外读。第二是每加一个指标就多一层子查询,比如再加「区域内排名」「按时间累计」,子查询会一层套一层,第二天自己都不敢改。第三是容易漏 JOIN 条件,一旦 ON 写错,结果会悄悄变成笛卡尔积,行数翻倍,而且不报错——这种错误最难查。

2.3 窗口函数的一句话版本:算归算,行不合并

同样的需求,换成窗口函数是这样写的:

SELECT sale_id, region, salesperson, amount, -- OVER 括号里描述「这次计算关心哪些行」:先按 region 分组,组内不动原始行 SUM(amount) OVER (PARTITION BY region) AS region_total, -- 同一份汇总值可以直接参与行内计算 ROUND(amount / SUM(amount) OVER (PARTITION BY region) * 100, 2) AS pct_of_region, -- 再加一个排名,仍然不需要子查询 RANK() OVER (PARTITION BY region ORDER BY amount DESC) AS rnk FROM sales_demo;

这段代码返回的行数和原表完全一样,一行不多一行不少,只是每行后面多了三列计算结果。关键就在OVER这两个字母:它告诉数据库「先按某个规则划出一批相关的行,在这批行上算一个值,然后把值贴回当前行」。PARTITION BY负责划线,ORDER BY负责线内排序,两者都不改变最终输出的行数。我第一次把子查询版本改写成这个写法时,最大的感受不是「快」,而是「终于能一眼看懂了」——上面这段从上往下读,就是一句正常的话。

2.4 速查表按「用途」分类,这一点对新手很关键

新手查资料最怕的是「知道要什么,但不知道它叫什么」。按字母排序的函数手册对熟手友好,对新手几乎是灾难。这份速查表的组织方式先讲基本概念,再讲语法结构,也就是function_name(...) OVER (...)这层外壳,然后把函数按排名、前后取值、聚合三大类摆开,每类下面配示例代码和返回结果。这意味着你查它的路径是「我想干什么」而不是「我记得它首字母是 R」。

举个具体例子。你想取每个区域金额最高的那一笔,脑子里没有函数名,翻开排名函数那一页,三个候选并排出现,各自的差异用一两句话写清楚,你当场就能选出该用哪个。这种「按意图检索」的设计,比背十个函数名有用得多,也是这份资料对零基础读者最友好的地方。

2.5 哪些人真的会天天用到它

做报表和维护数据库的人用得最多:算同比环比、算累计、找每组的最新一条,都是窗口函数的典型场景,写成一行比写三层子查询省事得多。做数据分析的人用它找异常:先算每组的平均值和标准差,再看哪一行偏离得离谱,一行 SQL 就能筛出可疑记录。写业务系统的人用它生成动态视图,比如排行榜、最近三次登录记录。教学场景里它也很好用,因为它把「分组」和「保留明细」这两件事拆得特别清楚,是理解 SQL 执行逻辑的好抓手。

3. 从零跑通第一个例子:四步让 OVER 出结果

概念听懂了不等于手会动。这一章从头建一张只有六行的小表,一步步把窗口函数跑出来,每一步都给你完整可复制的代码,以及「跑完之后应该看到什么」。建议真的打开一个数据库客户端跟着敲一遍,六行数据,出错也不心疼。手边任何一个支持窗口函数的数据库都行,语法细节可能略有差异,但整体结构一致。

3.1 第一步:建一张够小的练习表

练习表的原则是字段少、行数少、每行都能一眼看懂。下面这张表只有四个字段、六行数据,故意让两个区域里出现金额相同的记录,方便后面观察并列排名的行为。

-- 建一张练习用的销售明细表,字段尽量少,方便看清结果 CREATE TABLE sales_demo ( sale_id INTEGER, -- 每笔记录的唯一编号 region VARCHAR(20), -- 区域,用来做分组 salesperson VARCHAR(20), -- 销售人员 amount DECIMAL(10,2) -- 成交金额,保留两位小数 ); INSERT INTO sales_demo (sale_id, region, salesperson, amount) VALUES (1, '华东', 'A同学', 1200.00), (2, '华东', 'B同学', 800.00), (3, '华东', 'C同学', 1500.00), (4, '华北', 'D同学', 900.00), (5, '华北', 'E同学', 900.00), (6, '华北', 'F同学', 2100.00);

字段类型不用纠结:INTEGER存整数编号,VARCHAR(20)存短文本,DECIMAL(10,2)存带两位小数的金额。金额别用浮点类型,做除法时容易出现0.30000000000000004这种尾巴,新手看到会以为是自己写错了。建完表先SELECT * FROM sales_demo;看一眼,确认六行都在,再往下走。

3.2 第二步:先跑没有 OVER 的版本,建立对照组

在写窗口函数之前,先跑一遍普通聚合,记住它返回几行。这一步是为了让你后面能明显感觉到差别。

-- 对照组:普通聚合,两个区域只返回两行 SELECT region, SUM(amount) AS region_total FROM sales_demo GROUP BY region;

结果应该是两行:华东 3500,华北 3900。销售人员、单号、单笔金额全部消失。把这个结果记在心里,因为下一步要对比的就是它。很多人学窗口函数卡住,不是因为语法难,而是因为从来没认真看过 GROUP BY 到底丢掉了什么。

3.3 第三步:加上 OVER,让每一行都带着汇总值

现在把SUM后面的OVER (PARTITION BY region)加上去,其他一个字都不改。

SELECT sale_id, region, salesperson, amount, -- PARTITION BY region:按区域划定窗口,区域内所有行参与求和 SUM(amount) OVER (PARTITION BY region) AS region_total, -- 聚合结果可以直接和当前行的 amount 做除法 ROUND(amount / SUM(amount) OVER (PARTITION BY region) * 100, 2) AS pct_of_region FROM sales_demo;

跑完你会看到六行,一行没少。每一行的region_total都是该区域的总额,华东三行都是 3500,华北三行都是 3900。pct_of_region是当前这笔金额占区域总额的百分比,华东三行加起来正好 100。这就是窗口函数最核心的行为特征:计算结果按「窗口」来,输出行数按「原表」来。

参数逐个说一下。SUM(amount)是要算的东西,和普通聚合写法完全一样。OVER是关键字,出现它就代表「接下来是窗口定义」。PARTITION BY region是划分规则,意思是「区域相同的行归为一组」,如果不写这一句,整张表就是一个大窗口,六行的region_total都会是 7400。这三个部分里的任何一个改动,都会立刻改变结果,所以改完一定要重新看一遍输出。

3.4 第四步:在 OVER 里加 ORDER BY,看它悄悄改变了什么

这一步是新手最容易踩坑的地方。把ORDER BY放进OVER括号里,SUM的含义会从「整组总额」变成「从组内第一行累加到当前行」。

SELECT sale_id, region, amount, -- 注意:OVER 里加了 ORDER BY,SUM 就变成累计求和 SUM(amount) OVER (PARTITION BY region ORDER BY amount) AS running_total, -- 想看整组总额,就不要在括号里写 ORDER BY SUM(amount) OVER (PARTITION BY region) AS region_total FROM sales_demo;

跑出来对比一下这两列:region_total每行都一样,running_total会随着金额从小到大一路累加,最后一行正好等于该区域总额。原因在于ORDER BY一旦出现,数据库就会采用一个默认的窗口帧——从组内第一行到当前行。这个默认行为是标准规定的,不是哪个数据库的怪癖,但它足够隐蔽,很多人第一次遇到会以为自己算错了。

3.5 跑完之后怎么确认自己写对了

不要只盯着一行结果看,用三个动作快速自检。第一,数行数:窗口函数不改变行数,如果输出比原表多,多半是 JOIN 写错了或者漏了条件。第二,看分组总额:同一分组内,不带ORDER BY的聚合列应该每行完全相同,如果不一样,说明PARTITION BY的列选错了。第三,验算占比:把同一组的占比加起来,应该正好是 100,差一点点是四舍五入,差很多就是分母写错了。这三个动作养成习惯,能省掉大量来回问人的时间。

4. 三类窗口函数怎么分工:排名、前后取值与滚动聚合

速查表把函数分成排名、行序号、聚合三大类,这个分法不是学术分类,而是按「你想解决什么问题」来的。这一章把三类各挑最常用的几个讲透,每个都配一段能在刚才那张练习表上直接跑的代码。跑完这一章,速查表里八成的条目你都能看懂在说什么。

4.1 排名三兄弟:ROW_NUMBER、RANK、DENSE_RANK 差在哪

这三个函数长得像、用起来更像,但遇到金额相同的行时表现完全不同。练习表里华北的 D 同学和 E 同学都是 900,正好用来观察并列行为。

SELECT sale_id, region, salesperson, amount, ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount DESC) AS rn, -- 连续编号,不管并列 RANK() OVER (PARTITION BY region ORDER BY amount DESC) AS rk, -- 并列同名次,后面跳号 DENSE_RANK() OVER (PARTITION BY region ORDER BY amount DESC) AS drk -- 并列同名次,后面不跳号 FROM sales_demo;
函数并列时的表现华北三名销售的名次序列典型用途
ROW_NUMBER强行给出不同编号,并列时谁在前由数据库决定1、2、3每组只取一条、去重
RANK并列同名次,后续名次跳号1、2、2(下一名是 4)正式排名,允许名次空缺
DENSE_RANK并列同名次,后续名次不跳号1、2、2(下一名是 3)分档、分层,想要连续档位

新手最常问的是「到底该用哪个」。判断标准很简单:如果你要的是「每组挑一条出来」,用ROW_NUMBER,因为它保证编号唯一,取= 1不会多出一行。如果你要展示给业务方看名次,并且希望并列的人同分同名次,用RANK。如果你要把人分成「第一档、第二档」这种连续档位,用DENSE_RANK。选错的后果不严重但很尴尬——用ROW_NUMBER做排名,两个同分的人会被强行分出先后,业务方一眼就看出来不对劲。

4.2 LEAD 和 LAG:给表装一个「上一行」按钮

排名解决的是「我在组里排第几」,LEAD和LAG解决的是「上一行是什么」。它们按ORDER BY指定的顺序,往前或往后取第 n 行的值,默认 n 是 1。做环比、算相邻两次的差值,全靠这两个。

SELECT sale_id, region, amount, -- 取当前行前面那一行的金额,第一行没有前行,返回 NULL LAG(amount) OVER (PARTITION BY region ORDER BY sale_id) AS prev_amount, -- 取后面那一行的金额,最后一行返回 NULL LEAD(amount) OVER (PARTITION BY region ORDER BY sale_id) AS next_amount, -- 和上一行的差值,就是相邻两笔的差额 amount - LAG(amount) OVER (PARTITION BY region ORDER BY sale_id) AS diff FROM sales_demo;

第一个参数是要取值的那一列,第二个参数可以指定偏移几行,写成LAG(amount, 2)就是往前看两行,第三个参数是取不到时的默认值,不写就是NULL。新手常见的一个小坑是拿NULL去参与加减,结果整列都变成空值,其实是第一行没有前行导致的。想避开就在第三个参数上写个 0,或者在外面套一层COALESCE。这个「默认值」的位置在速查表里通常只用一行字带过,但它决定了你要不要多写一层清洗。

4.3 聚合函数加 OVER:累计求和、移动平均、占比一次说完

SUM、AVG、COUNT、MIN、MAX这五个函数加上OVER就变成了窗口函数,行为取决于括号里怎么写。不带ORDER BY是整组汇总,带ORDER BY是从第一行累加到当前行,再加上帧子句就能框住「最近 N 行」。

SELECT sale_id, region, amount, -- 不带 ORDER BY:整组总额,每行相同 SUM(amount) OVER (PARTITION BY region) AS region_total, -- 带 ORDER BY:从组内第一行累加到当前行 SUM(amount) OVER (PARTITION BY region ORDER BY sale_id) AS running_total, -- 加帧子句:只看当前行和它前面两行,算移动平均 AVG(amount) OVER (PARTITION BY region ORDER BY sale_id ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS moving_avg_3 FROM sales_demo;

第三列moving_avg_3是移动平均,括号里的ROWS BETWEEN 2 PRECEDING AND CURRENT ROW读作「从往前两行到当前行」,一共三行。组内第一行只有自己,平均值就是它本身;第二行是前两行的平均;从第三行开始才是三行平均。移动平均在处理波动大的数据时特别有用,比如看最近三次的成交趋势,比单看一笔稳得多。

4.4 窗口帧:最容易写错,也最少人讲清楚的部分

帧子句(frame clause)决定「窗口里到底包含哪几行」,写在ORDER BY后面。不写的时候有两条默认规则:带ORDER BY时默认是从组内第一行到当前行,不带ORDER BY时默认是整组。这两条默认规则能覆盖大部分场景,所以很多人一直没注意过它的存在,直到需要算移动平均才发现必须手写。

帧写法含义常见用途
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW从第一行到当前行累计求和,和不写帧时的默认行为一致
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW往前两行到当前行,共三行三次移动平均
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING前后各一行,共三行居中平滑
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING整组强制按整组算,抵消 ORDER BY 的影响

ROWS和RANGE的区别值得记一句:ROWS按物理行数数,RANGE按排序值数,遇到并列值时RANGE会把并列的行一起算进去。新手阶段统一用ROWS就好,它的行为直观、不容易算错。等你能稳定写出累计和移动平均,再回头研究RANGE也不迟。

4.5 一张表理清「什么时候用哪个」

你想做的事该用的函数关键写法
每组取一条(如最新一条记录)ROW_NUMBERPARTITION BY 分组列 ORDER BY 时间列 DESC,外层筛= 1
展示名次,并列同名次RANK或DENSE_RANK只看是否允许名次跳号
算环比、和上一行比LAG记得给第三参数默认值,避免NULL
算累计、看趋势聚合函数 +OVERORDER BY后不写帧即默认累计
算移动平均AVG+ROWS BETWEEN明确写n PRECEDING,别依赖默认
算占比聚合函数 +OVER分母用不带ORDER BY的整组汇总

5. 新手最容易翻车的五个地方:现象、原因和补救办法

窗口函数的语法本身不长,但它的执行位置和默认行为跟普通查询不一样,所以错误往往不报错、只是结果不对。下面五条是我见过频率最高的,每条都按「看到什么现象、为什么会这样、怎么改」来说,对照着检查能省下不少来回折腾的时间。

5.1 在 WHERE 里直接用窗口函数的结果

现象:写WHERE rn = 1,数据库直接报错,提示列不存在或者不允许在这里使用窗口函数。

原因:SQL 的执行顺序里,WHERE在SELECT之前执行,而窗口函数是在SELECT阶段才算出来的。也就是说WHERE跑的时候,rn这个别名还不存在。这不是哪个数据库的脾气,是标准规定的顺序。

解决:把带窗口函数的查询包成子查询或者公共表表达式,在外层再过滤。

-- 错误写法:WHERE 里引用别名,会直接报错 -- SELECT sale_id, ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount DESC) AS rn -- FROM sales_demo WHERE rn = 1; -- 正确写法:先算出排名,外层再筛 SELECT * FROM ( SELECT sale_id, region, salesperson, amount, ROW_NUMBER() OVER (PARTITION BY region ORDER BY amount DESC) AS rn FROM sales_demo ) AS t WHERE rn = 1; -- 每个区域只留金额最高的那条

包一层之后,rn是真实存在的列,WHERE就能正常用了。同理,GROUP BY和HAVING里也不能直接引用窗口函数结果,都要靠外层过滤。

5.2 排名用了 RANK,结果取每组第一条却多出好几行

现象:用RANK()算排名,外层筛rk = 1,本来以为每组只出一条,结果并列的那几条全出来了。

原因:RANK遇到并列值会给相同名次,rk = 1自然就命中多行。这不是 bug,正是RANK的设计意图,只是和「每组取一条」的需求不匹配。

解决:需要唯一一条就用ROW_NUMBER;如果确实想保留并列并且能接受多条,再用RANK。取唯一记录时还要注意ORDER BY要把并列打破,比如金额相同再按sale_id排一次,否则哪条被选中是不确定的,跑两次结果可能不一样。

5.3 OVER 里漏了 ORDER BY,排名每次跑出来都不一样

现象:同一段 SQL 连跑两次,ROW_NUMBER给出的编号不一样,明明数据没动过。

原因:ROW_NUMBER必须知道「按什么顺序编号」,ORDER BY缺失或者排序值有大量重复时,数据库没有义务保证稳定顺序。行在物理存储里的位置可能因为插入、更新、索引重建而改变,编号跟着变。

解决:给ORDER BY加一个能唯一定位的列做兜底,通常是主键或时间戳加主键。这个习惯值得刻进肌肉记忆里——排序不唯一的窗口函数,结果就是不稳定的,跟玄学没关系,纯粹是没告诉数据库该怎么排。

5.4 加了 ORDER BY 之后 SUM 变成累计,误以为算错了

现象:想在每行后面加一列「区域总额」,写完发现这一列越往下越大,最后一行的值才等于总额。

原因:OVER括号里写了ORDER BY,聚合函数就从「整组汇总」变成了「累计到当前行」,这是标准规定的默认帧行为。

解决:想看整组总额就删掉ORDER BY;如果确实需要同时看累计和总额,就写两个窗口,一个带ORDER BY一个不带。这个坑几乎每个新手都会掉一次,掉过之后就记住了:OVER括号里每一处增减都有含义,不是装饰。

5.5 大表上到处开窗,查询慢到像卡死

现象:在小表上跑得挺快,换到几十万行的表,查询要等很久甚至超时。

原因:窗口函数要在数据准备好之后才能计算,如果先对全表开窗再过滤,等于让数据库先处理完所有行。另外多个窗口定义不一致时,可能触发多轮排序,成本叠加。

解决:先把不需要的行用WHERE筛掉,再做窗口计算,能让参与排序的行数少一大截。多个窗口如果PARTITION BY和ORDER BY完全一致,可以写成命名窗口复用,减少重复排序。写完之后先在小样本上验证逻辑,再放到全量数据上跑,这个顺序别反过来。

6. 下一步学什么:把速查表当路线图,一周走完三轮

拿到一份速查表,最常见的用法是「遇到问题翻一下」,这没错,但对零基础的人来说,光靠查是建立不起手感的。我的建议是把它当成一周的路线图,分三轮走,每轮目标不同。

第一轮用两天,只做一件事:照着抄。每天从速查表里挑三到四个函数,在练习表上原样敲一遍,跑出来看一眼结果,然后改一个参数再跑一遍。改PARTITION BY的列、改ORDER BY的方向、改帧里的数字,看结果怎么变。这一轮不追求理解,追求的是「手熟」和「看到报错不慌」。

第二轮用三天,换成自己的真实问题。挑三个你平时用表格软件手工算过的场景:每组取最新一条、算环比、算累计占比。用窗口函数重写一遍,写完和手工结果对一次数。对上了,这个函数才算真的会了。对不上,正好是最好的学习机会,回头翻速查表对应的那一页。

第三轮用两天,回到语法那一节补细节。DISTINCT和ALL在窗口函数里的含义、命名窗口WINDOW w AS (...)怎么复用、ROWS和RANGE在并列值上的差别。这些内容第一轮看会懵,跑过十几个例子之后再回来看,会发现每一句都能对上具体的经历。

时间重点每天的量检验标准
第 1 至 2 天照着抄,改参数三到四个函数能不看资料默写出OVER (PARTITION BY ... ORDER BY ...)
第 3 至 5 天重写自己的真实需求每天一个场景结果和手工算的对得上
第 6 至 7 天补语法细节与帧子句速查表语法那一节能说清带不带ORDER BY的区别

最后说一个我自己的习惯。刚开始学的时候我总想一次写对,结果一个复杂查询改半小时还不敢跑。后来改成每加一个窗口就先单独SELECT出来看三行,确认这一列是我要的,再往上叠下一个。这个「小步验证」的习惯救过我很多次,尤其是PARTITION BY的列写错时,单独跑一眼就能看出来,省掉了事后排查的功夫。希望帮到你。

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

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

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

立即咨询