简介:《SQL窗口函数速查表》是一份面向数据库管理员、数据分析师、开发人员及数据科学家的实用PDF文档,系统梳理了SQL窗口函数的核心概念、语法结构、三大类型(排名、行序号、聚合)及典型应用场景。读者可借此快速掌握ROW_NUMBER()、RANK()、DENSE_RANK()、LEAD()、LAG()与SUM/AVG等函数的用法,在数据查询、报表生成、复杂数据集分析与教学科研中直接查阅参考。资源包仅含1个PDF文件,大小841KB,内容按功能分类组织,附示例代码与返回结果说明,便于快速定位和记忆。已有255人浏览学习,适合希望提升SQL查询效率的中级及以上技术人员。无论用于日常数据库管理、统计分析,还是应用程序开发,这份速查表都能帮助读者在实战中构建更高效、更清晰的SQL语句。
1. 窗口函数到底是什么:一张藏在SQL里的“透视表”
如果你用SQL做过数据分析,大概率遇到过这种场景:想算“每个部门里工资排名前三的员工”,或者“每个用户最近三笔订单的金额走势”。用GROUP BY只能得到汇总结果——每个部门一个数字;想保留每一行原始明细的同时,又把排名、累计值、前后行的值一起算出来,普通聚合函数做不到,这时候窗口函数就登场了。打个比方,GROUP BY像把一堆零件熔成一个成品,窗口函数却像给每个零件贴上一张标签——零件还是那个零件,但标签上写着它在整条流水线里的位置、前后邻居是谁。这份《SQL窗口函数速查表》就是把这些“标签规则”整理成一张表:哪些函数能用、怎么写OVER子句、每个参数什么意思。搞懂它,你写复杂查询时就不再靠玄学调SQL,而是看一眼语法就知道该用谁。
2. 为什么需要窗口函数:一道GROUP BY解不了的计算题
2.1 一个具体场景:月度销售额的“环比增长率”
假设你维护一张销售表sales(month, region, amount),存了各区域每个月的销售额。老板问你:每个区域每个月相比上个月增长了多少百分比。这是个典型需求:环比增长 = (本月金额 - 上月金额) / 上月金额。用普通聚合函数怎么算?你首先想到GROUP BY month, region拿到每个月的汇总金额,得到的是类似“华东区 1 月 100 万”这样的结果。但问题来了:要算“相比上月”,你得把上个月的金额作为一列和新月份并排放在同一行里,GROUP BY做不到——它只能让你看到每个月的汇总值,却没法让某一行“回头”去看它前面那行的值。
你可能会想用自连接:把销售表跟它自己按月份错位连接。写起来特别绕:要处理1月没有上个月、要小心笛卡尔积、查询一长串,维护起来也痛苦。更麻烦的是,如果以后需求变成“每个区域最近三个月累计”,自连接方案基本要重写。窗口函数存在的意义,就是把这些“行与行之间的计算”变成SQL的常规操作,而不是每次靠临时拼写JOIN来完成。
2.2 GROUP BY只回答“汇总结果”,不回答“逐行对比”
GROUP BY和窗口函数的核心差异,在于返回的行数不同。GROUP BY把多行压缩成一行,SELECT后面只能出现分组列和聚合函数的结果;窗口函数不改变返回行数,每一行原样保留,额外多出一列计算结果。这个区别,会彻底改变你写查询的思考方式。
举个例子。查询“每个区域的总销售额”,GROUP BY的写法是:
SELECT region, SUM(amount) AS total FROM sales GROUP BY region;结果每个区域一行,共 3 行(假设有 3 个区域)。想看到“每个区域每一天”的明细,同时带着“区域累计总额”,GROUP BY做不到——它要么只输出3行汇总,要么输出所有明细但没法同时带上总额。窗口函数的写法是:
SELECT month, region, amount, SUM(amount) OVER (PARTITION BY region) AS region_total FROM sales;返回的行数跟原始表完全一样,只是每一行旁边多了一列“这个区域的总金额”。这个模式你一定要记住:聚合函数 OVER (PARTITION BY 分组列)就是在不压缩行数的情况下,给每行附加一个分组聚合值。而region_total这一列的数值,会按区域重复——华东区的每一行都带着同一个华东区总额。这正是窗口函数的精髓:明细一个不少,汇总一个不落。
2.3 窗口函数如何同时保留“明细”和“汇总值”
方便起见,当你算“每个区域当月销售额”时,最常做的其实是下面这种:
SELECT month, region, amount, SUM(amount) OVER (PARTITION BY region, month) AS region_month_total, amount / SUM(amount) OVER (PARTITION BY region, month) AS region_month_share FROM sales;这里PARTITION BY region, month的意思是:按区域+月份把数据切分成一个个“窗口”,在每个窗口内计算SUM。于是每行既保留了自己的金额amount,又知道了自己在所属区域当月总金额里占了多少比例。这笔占比计算,用GROUP BY得先聚合、再JOIN回去拼明细,两段SQL来回倒腾;用窗口函数一条语句就完成了。
窗口函数还有一层GROUP BY永远做不到的能力:跨行引用。比如LEAD()能取当前行后面某行的值,LAG()能取前面某行的值。环比、同比、计算前后股价差值、查看用户上一次购买时间……这些统统属于“行与行之间的对比”,GROUP BY完全无法表达。从认知的角度讲,你可以把窗口函数理解为:SQL世界里专门负责“逐行上下文计算”的工具,而GROUP BY负责“分组汇总”。两者不是替代关系,是互补关系——什么时候用哪个,后面的章节会详细说。
3. 三类窗口函数怎么选:排名、错位取值与窗口聚合
速查表按功能把窗口函数分成了三类,这个分类本身就是一个很好的记忆框架。见到一个需求,先判断它属于“排名”“取前后行”还是“窗口内聚合”,再去找具体函数,效率会高很多。
3.1 排名三兄弟:ROW_NUMBER()、RANK()、DENSE_RANK()
排名需求是最常见的窗口函数应用。SQL里排名函数有三个,光记名字容易混,但它们的区别可以浓缩成一个场景:成绩表里有三个人并列第二名,接下来一个人是第五名还是第三名?
ROW_NUMBER():不管有没有重复,纯粹按顺序编号,1、2、3、4、5。它不关心并列,只保证每一行有一个唯一的递增序号。RANK():出现并列时跳过后续名次,1、2、2、4、5。三个人并列第二,下一个人是第5名,因为4被跳过了。DENSE_RANK():出现并列时不跳号,1、2、2、3、4。三个人并列第二,下一个人是第3名,跟物理世界的“并列排名大家都占同一个位次,但总数紧凑”一致。
SELECT student_name, score, ROW_NUMBER() OVER (ORDER BY score DESC) AS row_num, RANK() OVER (ORDER BY score DESC) AS rank_num, DENSE_RANK() OVER (ORDER BY score DESC) AS dense_num FROM exam_scores;这段SQL的重点在OVER (ORDER BY score DESC):它告诉数据库“先按成绩从高到低排序,再在这个顺序上分配名次”。三个函数返回同一张表的三个不同编号方式:row_num永远是连续的,rank_num有空洞,dense_num没有空洞。选择建议很简单——只要“唯一序号”选ROW_NUMBER();“竞赛排名”选RANK();“紧凑排名”选DENSE_RANK()。速查表里把这三个列在一起,就是为了让你在写排名时一眼看到差异,不用每次试跑才知道结果。
3.2 错位读取:LAG() 和 LEAD()
第二类函数解决的是“我想看到当前行之前或之后的某一行数据”。LAG()取前面第n行的值,LEAD()取后面第n行的值,默认n都是1。这两个函数是时间序列分析和“前后对比”的主力。
SELECT month, region, amount, LAG(amount, 1) OVER (PARTITION BY region ORDER BY month) AS prev_month_amount, LEAD(amount, 1) OVER (PARTITION BY region ORDER BY month) AS next_month_amount FROM sales;运行后,每个区域的每个月旁边会多出两列:prev_month_amount是本区域上个月的金额,next_month_amount是下个月的金额。第一行的prev_month_amount和最后一行的next_month_amount会是NULL,因为边界上不存在“前一行”或“后一行”。然后你就可以直接算环比:(amount - prev_month_amount) / prev_month_amount。注意这句SQL里PARTITION BY region是关键——如果没有它,LAG会跨区域取到别的地方的数据,结果完全错误。
这个函数的命名方向经常让人混淆,可以靠记单词解决:LAG有“滞后”的意思,取的是“过去/之前”的值;LEAD有“带领/超前”的意思,取的是“未来/之后”的值。在分析股票价格、用户行为序列、传感器数据时,LEAD和LAG几乎成了标准答案。
3.3 聚合函数当窗口函数用:SUM/AVG/COUNT/MIN/MAX
第三类是最容易被忽视的:普通的聚合函数SUM、AVG、COUNT、MIN、MAX,加上了OVER()之后也能变成窗口函数。区别在于,普通聚合会压缩行数,窗口版本不会。前面已经见过SUM的窗口用法,这里再看两个更进阶的。
第一个是移动平均。计算过去3个月的销售额平均值,用AVG配合ROWS BETWEEN子句:
SELECT month, amount, AVG(amount) OVER (ORDER BY month ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS moving_avg_3 FROM monthly_sales;这段SQL的意思是:对每个月,取它前面2行加上当前行,共3行,算平均值。结果中每行的moving_avg_3就是该月的三周期移动平均。这在做趋势分析时几乎是日常操作——股票K线、销售预测、网站流量分析都会用到。ROWS BETWEEN 2 PRECEDING AND CURRENT ROW这个写法要顺口记住,它是“滑动窗口”的核心表达。
第二个是全局占比。想直接看每个订单金额占全部订单的百分比,不用先聚合再JOIN:
SELECT order_id, amount, SUM(amount) OVER () AS total_amount, amount / SUM(amount) OVER () AS order_share FROM orders;OVER ()括号留空,代表整个结果集是一个窗口,每一行都能拿到全局总额total_amount,然后自己除以总额就是占比。这种“一行SQL解决占比/累计/移动计算”的能力,是窗口函数在数据分析和报表中受欢迎的核心原因。速查表特意把聚合函数单列成一类,就是想提醒你:这些函数你本来就会,只是多套了一个窗口语的壳子,功能就完全不同了。
4. OVER子句才是灵魂:PARTITION BY、ORDER BY与窗口边框
窗口函数的名字里已经暗示了一切:关键就在“窗口”二字,而窗口怎么划定,是由OVER()子句决定的。初步了解函数之后,你绕不开OVER()里的三个核心组件——分组、排序、边框。这章把它们拆开讲透。
4.1 语法骨架:函数 + OVER(三件套)
速查表给出的语法结构简化下来是:
function_name(expression) OVER ( PARTITION BY 分组列 ORDER BY 排序列 ROWS/RANGE BETWEEN 边框起止 )PARTITION BY决定数据怎么切分成块,ORDER BY决定每块内的先后顺序,ROWS/RANGE BETWEEN决定具体算哪些行的范围。三者都是可选项,但搭配起来千变万化。
快递分拣做个类比:PARTITION BY是把一麻袋快递按城市分成几个筐;ORDER BY是把每个筐里的快递按重量从轻到重排好;ROWS BETWEEN是告诉你“我只数这个筐里当前位置前面两个和后面一个,共四个”。三者一起工作,窗口函数才知道该把哪些行纳入计算。
还有一个重要细节:ORDER BY在窗口函数里和普通查询里的ORDER BY含义不同。普通ORDER BY只管最终结果的显示顺序,窗口函数里的ORDER BY却参与计算,比如RANK的排名顺序、LAG取哪一行,都必须依赖窗口内的ORDER BY。如果忘了写ORDER BY,排名函数会变成“全表随机分配号码”,因为默认没有顺序。这个坑几乎每个初学者都会踩一次。
4.2 PARTITION BY:分组但不折叠明细
PARTITION BY和GROUP BY的关系需要反复强调:GROUP BY压缩行数,PARTITION BY不压缩行数,只做“切块”。看个例子。
SELECT employee_name, department, salary, AVG(salary) OVER (PARTITION BY department) AS dept_avg_salary FROM employees;结果里每个员工仍在自己的那一行,dept_avg_salary列显示他所在部门的平均工资。同一个部门的每一行,dept_avg_salary数值相同。这种设计非常适合“给明细行同时提供部门指标”的场景,比如:找出工资低于部门平均水平的员工。
SELECT employee_name, department, salary FROM ( SELECT employee_name, department, salary, AVG(salary) OVER (PARTITION BY department) AS dept_avg FROM employees ) t WHERE salary < dept_avg;子查询计算出每个人的部门平均工资,外层查询直接筛选。如果用GROUP BY,你得先把部门平均值存临时表或子查询,再JOIN回员工表——多写几行代码不说,逻辑还绕。PARTITION BY存在的意义就是让“分组后的指标”和“原始明细”可以共存于同一行,这是它最实用的特性。
4.3 ORDER BY:窗口内的排序规则
窗口函数里的ORDER BY承担着双重角色。第一,它确定排名/编号的顺序;第二,它配合聚合函数时,能让聚合变成“累计值”。
SELECT month, amount, SUM(amount) OVER (ORDER BY month) AS cumulative_amount FROM monthly_sales;注意这里没有PARTITION BY,只有ORDER BY month。效果是:第一行的累计量等于第一行金额,第二行的累计量等于第一行+第二行,第三行继续往上加,形成一个逐月累计的序列。数据库默认的边框是RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,意思是“从窗口起点到当前行”,于是SUM变成了累加器。这个技巧在做累计销售额、累计活跃用户数、年初至今指标时,是一条非常标准的写法。
实际的注意事项:如果ORDER BY的列有重复值,数据库会把所有重复值的行合并到同一帧再计算——这可能导致累计值在同一个月份里看起来是“跳变”的。想精确控制每一行的累计范围,就要配合4.4的ROWS子句。这就是为什么速查表强调“ORDER BY+ 窗口边框”要一起理解。
4.4 frame子句:把窗口定义成“滑动区间”
ROWS BETWEEN和RANGE BETWEEN是控制“计算范围”的最后一块拼图。常用写法有:
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW:从分区起点到当前行(累计到当前行)ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING:当前行前后各两行,共5行(滑动的对称窗口)ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING:从当前行到分区末尾
SELECT month, amount, SUM(amount) OVER (ORDER BY month ROWS BETWEEN 3 PRECEDING AND CURRENT ROW) AS rolling_3m FROM monthly_sales;这里ROWS BETWEEN 3 PRECEDING AND CURRENT ROW取“当前行也就是本月,加上前面3个月”,窗口大小是4行。如果你希望的是“正好3个月”,那应该写2 PRECEDING。别小看这个-1的差别,很多人在写移动平均时,因为多算了一行而导致曲线偏平,查半天才反应过来。
ROWS和RANGE的区别也常让人头晕。一句话区分:ROWS按“物理行数”框选,说取几行就是几行;RANGE按“排序列的数值范围”框选,比如RANGE BETWEEN INTERVAL 30 DAY PRECEDING AND CURRENT ROW就表示“过去30天,不管期间有多少行”。时间序列上做“近30天求和”,RANGE比ROWS严谨得多。
4.5 一个综合示例:把三件套揉在一起
SELECT department, employee_name, salary, RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dept_rank, SUM(salary) OVER (PARTITION BY department ORDER BY salary DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS dept_cum_sum FROM employees;这句SQL同时算了两个指标:dept_rank是每个部门内部按工资降序的排名;dept_cum_sum是每个部门按工资降序排列后的累计工资额——最高薪员工的累计值就是他自己,第二高薪员工的累计值是两人之和,以此类推。同一张表,OVER()里同时套了排名函数和聚合函数,互不干扰。这就是窗口函数真正的威力:一次查询,多窗口并存,每个窗口独立计算、独立输出一列。
5. 易混概念避坑指南:5个新手最容易翻车的地方
窗口函数看着不难,实际跑起来经常和预期不符。这一章把最常见的5个混淆点列出来,你在用的时候可以逐条对照检查。
5.1 误区1:窗口函数会把结果“折叠”成一行
易混点:以为SUM(...) OVER (PARTITION BY ...)和GROUP BY一样,会输出每组一行。为什么混:写法里出现了PARTITION BY、聚合函数,长得和GROUP BY很像。怎么区分:看返回行数。窗口函数绝不减少返回行数,原始表有多少行,结果就有多少行。测试方法很简单——在查询里加一个COUNT(*) OVER (),如果每一行都能看到总行数,说明是窗口模式,不是聚合模式。如果哪次你发现结果行数少了,多半是忘了写OVER(),写成了普通聚合。
5.2 误区2:RANK()和DENSE_RANK()没区别
易混点:两个函数都给并列值相同的排名,看起来“差不多”。为什么混:成绩单里没有并列数据时,两者的输出完全一致,很多新手用普通数据测试,自然看不出差别。怎么区分:故意构造重复值看跳号情况。RANK是“竞赛排名”,并列后跳数字;DENSE_RANK是“紧凑排名”,并列后不跳。一个经典记忆:体育比赛里通常用RANK,因为并列第二后没有第四名,直接第五名;DENSE_RANK则适合那种需要知道“有多少个不同名次”的场景。速查表把这两个并列展示,目的就是逼你对比思考。
5.3 误区3:LAG()取的是“下一行”
易混点:把LAG当成“向后看”,LEAD当成“向前看”。为什么混:在时间序列的语境里,“前一个值”到底指过去还是未来,很容易想反。怎么区分:LAG的记忆锚点是“滞后”,时间轴上是过去的、排在前面的行;LEAD是“领先”,是未来的、排在后面的行。LAG(amount, 1)是上一行,LEAD(amount, 1)是下一行。判断方法:如果你查的是某个月的“上月值”,就用LAG;如果你查的是“下月值”,就选LEAD。写完之后建议立刻在数据里抽查一行,确认方向对了再继续往下写。
5.4 误区4:OVER里的ORDER BY决定最终输出顺序
易混点:认为RANK() OVER (ORDER BY score DESC)之后,查询结果就自动按分数排序了。为什么混:窗口函数里的ORDER BY确实参与了排名计算,看起来像“排好了”。怎么区分:窗口内的ORDER BY只决定窗口计算顺序,不保证结果集的物理顺序。要按分数排序显示,还需要在查询末尾单独写ORDER BY。一个反直觉但很常见的现象:结果里第一行不是分数最高的人,因为某些数据库的窗口计算和输出顺序是分开执行的。稳妥做法:需要显示顺序时,永远在查询末尾显式声明一次ORDER BY。
5.5 误区5:所有数据库的窗口函数都一样
易混点:在MySQL里写通的SQL,直接搬到别的数据库也认为能跑。为什么混:窗口函数已经是SQL标准的一部分,很多框架和ORM也统一封装,容易让人以为各个数据库等价。怎么区分:至少三处差异要留心。第一,函数命名有差异:有的数据库叫FIRST_VALUE,有的兼容,有的不支持。第二,ROWS和RANGE的默认行为在不同数据库里略有出入,表现尤其出现在有重复排序值的时候。第三,性能差异巨大:某些数据库窗口函数是优化器重点照顾的,某些是暴力排序,数据量一大,速度天上地下。速查表里附的示例基于标准SQL,换数据库前,建议先在目标环境的官方文档里核对语法。
6. 什么时候该用、什么时候别用:一张边界清单
窗口函数不是万能的,你得心里有本“取舍账”,才能少走弯路。
6.1 推荐使用的场景:一句话判断
只要需求属于下面三类之一,窗口函数就是首选方案。第一类,排名。员工工资排名、销售业绩Top N、每门课成绩前三名。第二类,行间对比。环比计算、同比计算、与上一单的时间间隔、账户余额的历史峰值。第三类,逐行附带汇总指标。每行明细旁边带上部门总额、全局占比,或者计算移动平均、累计值、年初至今累计。
一个判断口诀是:“如果这个数字只是挂在每一行上额外显示,就用窗口函数;如果输出结果的行数必须比原始行数少,再看GROUP BY。”比如“每个部门的总工资”——结果预期就是部门数,不是员工数,GROUP BY更合适。而“每个员工的工资 + 所属部门平均工资”,结果还是员工数,窗口函数显然更值得选。
6.2 别硬上的场景:窗口函数不是万能钥匙
有几类场景用窗口函数是给自己找麻烦。第一种,纯分组汇总需求。只需要部门总数、平均值,不关心明细行,GROUP BY表达更清晰、执行计划也更高效。第二种,窗口太大且无法用PARTITION BY缩小范围。窗口函数需要在内存或临时表里保存每个分区的数据,如果窗口覆盖整张上亿行的表,又碰巧没写PARTITION BY,性能可能直接崩掉——这种情况下,换个思路用物化表或分批查询反而更快。第三种,复杂过滤条件嵌套。虽然可以在窗口函数外再包一层子查询,但可读性代价很大。比如“选每个城市下单金额排名前3的用户且金额超过1000”,逻辑上要先过滤再排名,顺序一乱结果就错。我的习惯是:先WHERE过滤掉噪音数据,再上窗口函数,不要反过来。第四种,超大滑动窗口且对边界极其敏感。窗口边框处理NULL边界、重复值排序影响累计结果,调试成本很高。如果你的场景只是“每月汇总”,真的没必要引入它。
6.3 一个完整的取舍例子:累计销售额计算
需求:统计每个区域的年度累计销售额。窗口函数写法是这样:
SELECT month, region, amount, SUM(amount) OVER (PARTITION BY region, YEAR(month) ORDER BY month) AS yearly_cum FROM sales ORDER BY region, month;如果换成普通SQL,你得自己写一个相关子查询。但实际跑起来,一个看似复杂的场景最终选择了窗口函数——因为它的语义清晰、维护成本低。真正的切身体会:刚开始写SQL那阵,凡是碰到“累计”“排名”“上个月”这些词,我就套GROUP BY加临时表,绕得晕头转向,查询还慢。后来强迫自己遇到这类需求先问一句“结果行数变不变”,不变就一律先试窗口函数。从那以后,我几乎形成了一个条件反射——涉及排名、前后值、累计、移动计算,第一选择就是窗口函数,只有确认是纯分组汇总才回头用GROUP BY。这个习惯帮我省了大量调试时间。希望这份速查表能成为你手中的那个“反射开关”:翻到对应分类,找到函数,写下OVER子句,一条SQL就把问题讲清楚,而不是在堆临时表的路上反复踩坑。希望帮到你。
本文还有配套的精品资源,点击获取