简介:《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_totalROWS 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;跑出来的结果长这样:
| name | score | rn | rk | drk |
|---|---|---|---|---|
| A | 98 | 1 | 1 | 1 |
| B | 95 | 2 | 2 | 2 |
| C | 95 | 3 | 2 | 2 |
| D | 90 | 4 | 4 | 3 |
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。这类问题的后悔药不好买,提前想清楚最省事。希望帮到你。
本文还有配套的精品资源,点击获取