SQL窗口函数速查:解决排名、累计与滚动计算的高频套路
发布时间:2026/10/11 14:36:10 锦皓数字建站

简介《SQL窗口函数速查表》是一份面向数据库管理员、数据分析师、数据科学家及开发人员的PDF参考文档系统梳理窗口函数的基本概念、语法结构、参数说明与常见用法。内容重点涵盖ROW_NUMBER()、RANK()、DENSE_RANK()、LEAD()、LAG()以及SUM、AVG等聚合窗口函数并配有示例代码展示OVER子句中分组、排序与窗口范围设置。资源为单个PDF文件大小约841KB内容按功能分类组织便于快速定位与查阅也可作为SQL教学和备考复习的辅助材料。已有255人学习下载适合需要在数据处理、报表生成或复杂查询场景中提升效率的技术人员。借助这份速查表读者可以快速掌握各类窗口函数的具体用法和返回结果理解不同排名函数的差异并能够结合实际数据编写更高效的SQL查询语句。1. 窗口函数速查表解决 80% 复杂 SQL 计算问题的固定套路写过一段带排名的报表 SQL你就会发现 GROUP BY 有个绕不过去的坎它一旦分组原始行就消失了。想做「每个用户最近一笔订单」「销售额环比」「累计占比」这类计算要么写一堆子查询自连接要么把数据拉回应用层处理。窗口函数就是为这类场景设计的——它不折叠明细行而是把聚合结果摊到每一行旁边。这份《SQL窗口函数速查表》PDF把排名、行序号、聚合三大类函数的语法、参数和示例代码全部收在一份文档里按使用场景归类直接翻到对应分类就能抄作业。适合三类人数据分析师写日报统计时对照函数签名DBA 排查慢 SQL、重写低效子查询时查边界条件开发人员在业务代码里构造查询时当语法手册用。新手照着示例改列名和分区条件即可落地熟手可以用它快速确认不同数据库方言下的 frame 行为差异。下面按我拆解这份速查表的顺序把窗口函数从语法到实战到坑位完整过一遍。2. 窗口函数的核心语法与执行逻辑先理解开窗再写 OVER()2.1 从 OVER() 拆起PARTITION BY、ORDER BY 与 frame 三件套窗口函数的灵魂在 OVER() 括号里的三个子句。很多人上来就背函数名却忽略 OVER() 的书写规则结果 ROW_NUMBER 写出来编号乱跳 SUM 算出来是全局汇总而不是分组汇总。先记住一句话函数决定算什么OVER() 决定在哪些行上算。下面是最常见的一个完整写法统计每个用户按时间累计的订单金额SELECT user_id, order_date, amount, SUM(amount) OVER ( PARTITION BY user_id ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS cumulative_amount FROM orders;这段 SQL 的逻辑是先按 user_id 分成多个独立窗口每个窗口内按 order_date 升序排列然后从窗口第一行累加到当前行返回每个订单对应的累计金额。注意窗口函数的计算结果不会减少行数——orders 表有多少行结果就有多少行只是额外多了一列。我把三个子句的参数含义和默认行为整理成一个表这也是速查表里最值得反复看的部分子句作用省略时的默认行为典型参数PARTITION BY把结果集按字段拆成独立窗口不写则整张表视为一个窗口一个或多个字段支持表达式ORDER BY定义窗口内行的排序顺序不写则窗口内顺序不确定字段加 ASC/DESCROWS/RANGE frame限定参与计算的行的范围RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWROWS BETWEEN n PRECEDING AND m FOLLOWING这里有一个新手最容易误判的点ORDER BY 在这个场景里不只是用来排序它还参与定义了 frame 的边界。同样的 SUM(amount) OVER (PARTITION BY user_id)ORDER BY 写不写计算范围完全不同。没有 ORDER BY 时frame 会退化结果接近整个分组的普通聚合写了 ORDER BY 后默认只累加到当前行。这就是「累计」和「分组汇总」的区别所在。实际拆文档时我一般建议把 OVER() 的三个子句当成一道填空先想清楚要不要拆组再想清楚组内顺序最后想清楚要覆盖哪些行。三个问题回答完毕90% 的窗口函数语句都能直接写出来。2.2 窗口函数和 GROUP BY 的本质区别何时该用哪种窗口函数经常被拿来和 GROUP BY 对比很多入门者分不清两者边界。核心差异就一条GROUP BY 折叠行窗口函数保留行。举个例子统计每个用户的订单总金额。用 GROUP BY 写SELECT user_id, SUM(amount) AS total_amount FROM orders GROUP BY user_id;结果每个用户只有一行。但很多时候业务要的是「订单明细旁边跟着这个用户的总金额」比如做占比分析时想直接看到每个订单占该用户总额的百分比。这时 GROUP BY 的折叠特性反而成了障碍要么再 join 一次聚合结果要么用窗口函数SELECT user_id, order_id, amount, amount / SUM(amount) OVER (PARTITION BY user_id) AS order_share FROM orders;注意这里 SUM 的 OVER() 里没有 ORDER BY也没有 frame 子句所以它计算的是整个 user_id 分区的合计然后把这个合计复制到分区内每一行旁边再和当前行的 amount 做除法。这个写法的优势是少一次自连接SQL 语句更短执行计划通常也更简单。选型上我的习惯是如果最终结果需要明细行和聚合值同时出现优先窗口函数如果下游只消费汇总结果继续用 GROUP BY。还有一个折中方案先用 GROUP BY 缩小数据量再对聚合结果开窗性能通常比直接对明细开窗好。这在有 Spark SQL 或 Hive 跑大表的场景下尤其明显——Hive 的窗口函数语义和 MySQL、PostgreSQL 一致但数据量大时窗口内的排序开销不容忽视能用子查询先减数据量就先减。另外要提醒一点窗口函数不能直接出现在 WHERE 子句里。你写 WHERE ROW_NUMBER() OVER(...) 1 大概率直接报错因为 WHERE 在窗口计算之前执行。正确做法是包一层子查询这也是下一章要展开的操作。3. 排名类窗口函数实战ROW_NUMBER、RANK 与 DENSE_RANK 的分工3.1 三个排名函数在「并列名次」上的差异排名函数是窗口函数里使用频率最高的一类也是 SQL 面试题里的常客。ROW_NUMBER、RANK、DENSE_RANK 三个函数长得像行为差异集中在「遇到并列值怎么编号」。直接看对比表函数并列时的行为典型返回值适用场景ROW_NUMBER()并列值也强行连续编号1, 2, 3, 4去重、分页、取前 N 行RANK()并列值编号相同后续编号跳过1, 1, 3, 4竞赛排名强调并列名次占位DENSE_RANK()并列值编号相同后续编号不跳过1, 1, 2, 3并列不占位名次紧凑用一个成绩表实例验证。假设学生成绩为 98、95、95、90SELECT 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 student_scores;返回结果是98 分的 row_num1、rank_num1、dense_num1两个 95 分的 row_num 分别为 2 和 3rank_num 都为 2dense_num 都为 290 分的 row_num4rank_num4dense_num3。注意 RANK 跳过了 3而 DENSE_RANK 没有。参数边界也要说清楚这三个函数在 OVER() 里基本只需要 ORDER BYPARTITION BY 按需加如果连 ORDER BY 都不写MySQL 和 PostgreSQL 也会返回结果但行号顺序没有语义保证翻车概率极高。实际写业务 SQL 时这三个函数不能凭感觉选——取 Top N 用 ROW_NUMBER 最干净做并排名次展示用 DENSE_RANK 更符合直觉而 RANK 在需要「名次占位」的报表里才有价值。这份速查表把三者的差异列在一张表里对照着记一次就能分清。3.2 去重取最新ROW_NUMBER 在「每组去重」里的应用「每组取最新一条」是数据分析里最高频的需求之一也是 SQL 语句去重的进阶玩法。早期我写过不少自连接解法后来发现 ROW_NUMBER 加子查询的写法更直接也更符合执行逻辑。场景orders 表里同一天可能有多笔订单需求是取每个用户最近一笔订单的完整信息SELECT user_id, order_id, order_date, amount FROM ( SELECT user_id, order_id, order_date, amount, ROW_NUMBER() OVER ( PARTITION BY user_id ORDER BY order_date DESC, order_id DESC ) AS rn FROM orders ) t WHERE rn 1;逻辑拆解内层子查询按 user_id 分区每个分区内按订单日期倒序同日多笔时再按 order_id 倒序编号最新一笔 rn1外层 WHERE 过滤出 rn1 的行。这里的关键点是窗口函数在 WHERE 子句中不可用必须套一层子查询再做条件过滤这是语法层面的硬性约束。ORDER BY 部分有两个细节值得注意。第一排序字段越精细越稳定——只按 order_date DESC 排同一天两笔订单谁排第一不确定加上 order_id DESC 这样的唯一性字段做 tie-breaker结果即可复现。第二如果业务上要「每个用户金额最大的一笔订单」只需把 ORDER BY 改成 ORDER BY amount DESC其余结构不变。这个写法对慢 SQL 排查也很友好内层子查询走完排序后外层只是简单过滤执行计划清晰可见。对比传统写法——先按用户分组求最大日期再 JOIN 回原表——ROW_NUMBER 方案少一次 JOIN数据量大时性能差异明显。这也是速查表把 ROW_NUMBER 排在排名函数首位的原因它不只是编号工具更是重组数据结构的利器。4. 前后行取数与滚动聚合LEAD/LAG 与聚合窗口的正确打开方式4.1 LEAD 与 LAG用一行代码算环比和同期对比行序号函数 LEAD() 和 LAG() 的用途很纯粹取窗口内当前行之前或之后的第 n 行数据。它们可以把「自连接取上一行」的操作压缩成一个函数调用是计算环比、同比、前后值对比的首选工具。以每日订单金额的环比计算为例SELECT order_date, SUM(amount) AS daily_amount, LAG(SUM(amount), 1) OVER (ORDER BY order_date) AS prev_day_amount FROM orders GROUP BY order_date ORDER BY order_date;这里先按天聚合得到每日金额再用 LAG 取上一行——也就是前一天的金额。注意 LAG 的 OVER() 里没有 PARTITION BY因为聚合结果本身就是全量时间序列直接按 order_date 排序即可。第一个参数是目标列第二个参数是往前偏移的行数默认是 1。如果要算环比增长率可以继续嵌套SELECT order_date, daily_amount, prev_day_amount, ROUND( (daily_amount - prev_day_amount) / NULLIF(prev_day_amount, 0) * 100, 2 ) AS growth_rate FROM ( SELECT order_date, SUM(amount) AS daily_amount, LAG(SUM(amount), 1) OVER (ORDER BY order_date) AS prev_day_amount FROM orders GROUP BY order_date ) t;两个细节必须注意。第一LAG 的第三个参数可以指定默认值——当窗口内没有前一行时LAG 返回 NULL如果不希望结果里出现 NULL可以写 LAG(amount, 1, 0)这里我用 NULLIF 把分母为 0 的情况转成 NULL避免除零报错。第二LEAD 和 LAG 是反向对应关系LEAD 取后一行LAG 取前一行参数语义完全一致。实际业务里 LAG 更多配合 PARTITION BY 使用比如每个用户按时间排序后取该用户上一笔订单金额做消费行为分析。这时 PARTITION BY 和 ORDER BY 缺一不可——漏了 PARTITION BY 会把不同用户的数据混在一起比较结果失去业务含义。速查表把这两个函数归为一类是有原因的它们的参数结构相同差异只是方向记一个就能推另一个。4.2 聚合函数加 OVER滚动求和与累计占比的 frame 控制聚合类窗口函数是最被低估的一类。 SUM、AVG、COUNT、MIN、MAX 都可以加 OVER() 开窗而真正让它们产生质变的是 frame 子句——它精确控制「哪些行参与计算」。先看滚动求和场景计算每个订单日往前推两天的移动平均销售额三天滚动窗口SELECT order_date, daily_amount, AVG(daily_amount) OVER ( ORDER BY order_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW ) AS moving_avg_3d FROM daily_sales ORDER BY order_date;执行逻辑按 order_date 排序后当前行对应的窗口是「前 2 行 当前行」AVG 在这个窗口内计算每移动一行窗口也跟着滑动。ROWS BETWEEN 2 PRECEDING AND CURRENT ROW 是最常用的 frame 写法它明确告诉数据库「窗口边界在哪里」不依赖默认值。frame 子句的完整语法有两种边界定位方式写法含义典型用途ROWS BETWEEN n PRECEDING AND CURRENT ROW当前行往前 n 行到当前行移动平均、滚动求和ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW窗口起点到当前行累计金额、累计占比ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING当前行到窗口末尾剩余占比、倒序累计RANGE BETWEEN ...按排序字段的值范围定窗口时间区间统计累计占比是另一个高频用法。计算每月销售额占全年累计的比例SELECT month, sales_amount, SUM(sales_amount) OVER ( ORDER BY month ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS cumulative_amount, ROUND( sales_amount / SUM(sales_amount) OVER () * 100, 2 ) AS monthly_share FROM monthly_sales;这里出现了两个窗口第一个按月份递增累计第二个 OVER() 括号为空代表全表作为一个窗口计算总销售额。空 OVER() 是聚合窗口函数里容易被忽略的语法它的语义相当于把整张表的 SUM 复制到每一行旁边非常适合做占比计算。有一个边界点要提醒如果 OVER() 里写了 ORDER BY 但没写 frame 子句不同数据库对默认 frame 的处理不完全一致。主流数据库按 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW 处理意味着当前行到窗口起点之间与当前行排序值相同的行也会被包含进来而如果排序字段有大量重复值计算范围可能比你想的宽。所以当排序字段重复度高时建议显式写 ROWS 而不是依赖默认值。5. 窗口函数避坑记录三个让你查半天才发现的翻车点5.1 诡异结果ROW_NUMBER 的编号乱序现象在 MySQL 8.0 上执行 ROW_NUMBER() OVER (PARTITION BY user_id)同一分区内的行号次序不稳定几次查询结果不一致。原因OVER() 里只写了 PARTITION BY没有 ORDER BY。窗口函数要求「有序」才能有确定语义缺了 ORDER BY数据库按物理存储顺序输出行号自然就是随机的。解决给 OVER() 补上明确的 ORDER BY 字段如 ORDER BY order_date DESC如果明细里没有天然排序字段至少用一个唯一键保证输出可重复。从那以后我写任何排名函数都强制检查是否有 ORDER BY每秒都值得。5.2 累计值比预期偏大frame 默认值吃掉了重复行现象用 SUM(amount) OVER (PARTITION BY user_id ORDER BY order_date) 计算累计金额发现某些日期出现多条同值订单时累计值一次性包含了好几条记录而不是一条条累加。原因ORDER BY 下默认 frame 是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROWRANGE 按排序值定位边界——排序值相同的行全部落入当前窗口。同一天多笔订单时这些订单的累计值相同且一次算完。解决把 frame 显式写成 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW按行定位窗口边界一行一行累加。我在速查表里看到这一条时第一反应是「这个坑我也踩过」值得用荧光笔标出来。5.3 语法报错WHERE 里直接写了窗口函数现象执行 WHERE ROW_NUMBER() OVER (...) 1 直接报错「窗口函数不允许出现在 WHERE 中」。原因SQL 的执行顺序里 WHERE 在窗口计算之前执行。窗口函数的计算结果是个新的列只有 SELECT 和 ORDER BY 阶段能直接访问。这是语法层面的硬限制不是某个数据库的实现缺陷。解决把窗口查询包成子查询外层再过滤窗口结果SELECT * FROM ( SELECT *, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_date DESC) AS rn FROM orders ) t WHERE rn 1;同理窗口函数的别名也不能在 WHERE 里直接引用——这一点在 PostgreSQL、MySQL、SQL Server 上表现一致。养成「窗口函数永远在子查询里被消费」的习惯后这类报错基本绝迹。5.4 PARTITION BY 字段含 NULL数据凭空多了一组现象按 user_id 分区统计发现结果里出现一个 user_id 为 NULL 的分区且该分区数据量不小。原因PARTITION BY 把 NULL 视为一个独立分组不会与其他值合并。这是标准语义不是异常但业务上容易漏看。解决如果 NULL 表示「未登录的匿名用户」业务上需要单独处理可以在分区字段上加 COALESCE 归一化SUM(amount) OVER (PARTITION BY COALESCE(user_id, unknown))注意 COALESCE 的默认值要与真实数据不冲突否则会串组。这条经验在处理埋点数据时特别有用日志表里 user_id 为空的比例通常不低。6. 把速查表用得再狠一点用「写前自问」验证窗口函数结果工具书的价值在查不在读。速查表拿到手后我建议不要从头到尾看一遍就放在收藏夹吃灰而是把它当成一个「写窗口函数前的自检清单」。我结合表里的分类和语法结构整理了一套四连问每次写窗口逻辑前按顺序过一遍第一问我要保留明细行还是折叠成汇总行要明细行就开窗只要汇总行就用 GROUP BY。第二问窗口范围是什么先写 PARTITION BY 定分区再写 ORDER BY 定顺序最后检查是否需要显式写 frame——只要排序字段可能重复就把 ROWS 边界写死。第三问排序方向对不对ROW_NUMBER 里 DESC 是「最新在前」编号为 1ASC 是「最早在前」这两个方向在取最近记录的场景里正好是镜像关系。第四问结果要在 WHERE 里过滤吗凡是窗口函数的输出要被过滤或消费一律先包子查询。这套自问流程配合速查表能在动手写 SQL 之前拦截大部分问题。每次写完窗口函数后我做一次验证拿线上数据量最小的一段区间手工算一遍预期结果再和窗口函数输出比对。验证还不是终点——执行计划也要同步看如果 OVER(ORDER BY ...) 触发全量排序就需要考虑先用 GROUP BY 缩数据量。速查表里那一段「窗口函数能极大地提升查询效率」的表述有一个隐含前提窗口范围越小排序成本越低。大分区全表开窗时优化空间有限。从那以后我每次写涉及排名的查询都强制走一遍四连问再对照速查表确认 frame 边界。这个习惯帮我挡掉了至少十次逻辑错误也希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。