news 2026/10/9 10:04:15

窗口函数速查表:从分组TopN到累计求和,避开5个常见坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
窗口函数速查表:从分组TopN到累计求和,避开5个常见坑

简介:这份《SQL窗口函数速查表》PDF面向数据库管理员、数据分析师、数据科学家及开发人员,尤其适合希望提升复杂数据集查询能力的技术人员。内容按功能与用途分类,系统梳理了窗口函数的基本概念、语法结构与参数说明,涵盖ROW_NUMBER()、RANK()、DENSE_RANK()等排名函数,LEAD()、LAG()等行序号函数,以及SUM、AVG、COUNT、MIN、MAX等聚合函数,并配有示例代码帮助理解每个函数的用法与返回结果。资源包共1个PDF文件,大小约841KB,轻量便携,便于随时查阅与记忆。目前已有255人学习下载。读者可借助这份速查表快速定位所需函数,掌握PARTITION BY、ORDER BY与窗口帧等关键语法,在报表生成、数据探索、统计分析及应用程序查询构建中编写更高效的SQL语句,也可作为教学与学术研究的参考材料。

1. 为什么你背了语法还是写不对窗口函数

窗口函数这东西,语法半小时就能背完,但真正写起来翻车的概率高得离谱。我见过太多人ROW_NUMBER()、RANK()、SUM() OVER()背得滚瓜烂熟,一到真实业务查询就写出全表扫描、结果错位、分组串行的 SQL。问题不在语法记忆,在于没搞清楚「窗口」到底是怎么划出来的——PARTITION BY切的是逻辑分区,ORDER BY定的是分区内的行序,而ROWS/RANGE决定的是当前行能看到多宽的范围。这三层叠在一起,才是窗口函数的完整语义。

这篇速查表不是语法罗列,而是按「你实际会遇到的查询场景」来组织的:排名去重、累计求和、同比环比、滑动平均、分组取 TopN。每个场景给出可直接抄的 SQL、参数含义、以及我踩过的坑。适合已经会写基础 SQL、但一碰到窗口函数就靠试错的人。读完你应该能做到:看到需求就知道该用哪个窗口函数、窗口帧怎么定、性能瓶颈在哪。

2. 窗口函数的三个核心参数:PARTITION BY、ORDER BY、窗口帧

2.1 PARTITION BY:决定数据怎么切块

PARTITION BY是窗口函数的第一道分界线。它把结果集按指定列切成若干逻辑分区,窗口函数在每个分区内独立计算。不写PARTITION BY时,整个结果集就是一个大分区。

-- 按部门分区,计算每个部门内每个员工的薪资排名 SELECT dept_id, emp_name, salary, ROW_NUMBER() OVER ( PARTITION BY dept_id -- 按部门切块 ORDER BY salary DESC -- 部门内按薪资降序 ) AS salary_rank FROM employee_salary;

逻辑说明:PARTITION BY dept_id让每个部门成为独立计算单元,ROW_NUMBER()在每个部门内从 1 开始编号。如果不写PARTITION BY,整个表一起排名,跨部门混在一起,结果就错了。

参数说明:PARTITION BY后面可以跟多列,用逗号分隔,比如PARTITION BY dept_id, job_level。分区列的选择直接决定结果粒度——你想在哪个维度内做排名/累计,就按哪个维度分区。常见错误是把不该分区的列放进去,导致每个分区只有一行,窗口函数退化成普通聚合。

2.2 ORDER BY:决定分区内的行序

ORDER BY在窗口函数里和普通查询的ORDER BY语义不同。普通ORDER BY决定最终输出顺序,窗口内的ORDER BY决定函数按什么顺序处理行。对于ROW_NUMBER()、RANK()、LAG()、LEAD()这类函数,ORDER BY是必须的;对于SUM()、AVG()这类聚合窗口函数,ORDER BY决定了累计的方向。

-- 按日期累计求和:ORDER BY 决定累计方向 SELECT sale_date, amount, SUM(amount) OVER ( ORDER BY sale_date -- 按日期升序累计 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS cumulative_sum FROM daily_sales;

逻辑说明:ORDER BY sale_date让行按日期排列,SUM从第一行累加到当前行。如果改成ORDER BY sale_date DESC,累计方向反过来,变成从最新日期往回累加。

参数说明:窗口内的ORDER BY支持ASC/DESC,也支持多列。注意,当ORDER BY存在但没写窗口帧时,默认帧是RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,这意味着所有与当前行ORDER BY值相同的行会被一起纳入计算——这是很多人算累计值时结果偏大的根本原因。

2.3 窗口帧:ROWS 和 RANGE 的区别

窗口帧是窗口函数里最容易翻车的地方。ROWS按物理行偏移,RANGE按ORDER BY的值偏移。默认行为取决于有没有ORDER BY:

条件默认窗口帧含义
无 ORDER BY整个分区所有行参与计算
有 ORDER BYRANGE UNBOUNDED PRECEDING TO CURRENT ROW从分区第一行到当前行 ORDER BY 值相同的所有行
-- ROWS vs RANGE 在滑动窗口中的差异 SELECT sale_date, amount, -- ROWS:严格按物理行数取前 2 行 + 当前行 SUM(amount) OVER ( ORDER BY sale_date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW ) AS sum_rows, -- RANGE:按日期值取前 2 天 + 当前天(可能包含多行) SUM(amount) OVER ( ORDER BY sale_date RANGE BETWEEN INTERVAL '2' DAY PRECEDING AND CURRENT ROW ) AS sum_range FROM daily_sales;

逻辑说明:ROWS BETWEEN 2 PRECEDING AND CURRENT ROW严格取当前行往前数 2 行,不管日期是否连续。RANGE BETWEEN INTERVAL '2' DAY PRECEDING AND CURRENT ROW取日期在 [当前日期-2天, 当前日期] 范围内的所有行,如果某天有多条记录,全部纳入。

参数说明:ROWS的偏移量是整数,RANGE的偏移量是ORDER BY列的同类型值。UNBOUNDED PRECEDING表示分区起点,UNBOUNDED FOLLOWING表示分区终点,CURRENT ROW表示当前行。选ROWS还是RANGE取决于业务语义:按行数滑窗用ROWS,按时间/数值范围滑窗用RANGE。

提示:MySQL 8.0 之前不支持窗口函数,PostgreSQL、SQL Server、Oracle 的支持更早。如果用的是 MySQL 5.7,只能通过变量模拟,性能和可读性都差很多。

3. 五个高频场景的窗口函数写法与参数调优

3.1 分组 TopN:ROW_NUMBER 还是 RANK

分组取 TopN 是最常见的窗口函数需求。核心思路是用ROW_NUMBER()或RANK()打标,再在外层过滤。

-- 每个部门薪资最高的 3 名员工 WITH ranked AS ( SELECT dept_id, emp_name, salary, ROW_NUMBER() OVER ( PARTITION BY dept_id ORDER BY salary DESC ) AS rn FROM employee_salary ) SELECT dept_id, emp_name, salary FROM ranked WHERE rn <= 3;

逻辑说明:CTE 里先按部门分区、薪资降序打行号,外层筛rn <= 3。ROW_NUMBER()保证每行唯一编号,即使薪资相同也不会并列。

参数说明:如果薪资相同需要并列排名,用RANK()——相同薪资给相同排名,下一个排名跳号(1,1,3);用DENSE_RANK()则跳号不跳位(1,1,2)。选哪个取决于业务:严格取 3 个人用ROW_NUMBER(),允许并列用RANK(),并列后仍要连续名次用DENSE_RANK()。

3.2 累计求和与移动平均:窗口帧怎么定

累计求和和移动平均是窗口帧的典型应用。累计求和用UNBOUNDED PRECEDING TO CURRENT ROW,移动平均用N PRECEDING TO CURRENT ROW。

-- 7 日移动平均(按物理行) SELECT sale_date, amount, AVG(amount) OVER ( ORDER BY sale_date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW ) AS ma_7d FROM daily_sales; -- 按日期范围的 7 日移动平均(处理日期不连续) SELECT sale_date, amount, AVG(amount) OVER ( ORDER BY sale_date RANGE BETWEEN INTERVAL '6' DAY PRECEDING AND CURRENT ROW ) AS ma_7d_range FROM daily_sales;

逻辑说明:第一个查询严格取当前行往前 6 行,如果中间有日期缺失,实际覆盖的不是 7 天。第二个查询按日期值取 7 天范围,日期不连续也能正确覆盖。

参数说明:ROWS适合数据行连续且无缺失的场景,RANGE适合按时间/数值范围滑窗的场景。注意RANGE在某些数据库中对INTERVAL的支持有限,PostgreSQL 支持较好,MySQL 8.0 也支持但语法略有差异。

3.3 同比环比:LAG 和 LEAD 的偏移参数

同比环比用LAG()和LEAD()取前/后 N 行的值,再做差值或比值。

-- 月度环比:本月 vs 上月 SELECT month, revenue, LAG(revenue, 1) OVER (ORDER BY month) AS prev_month_revenue, revenue - LAG(revenue, 1) OVER (ORDER BY month) AS mom_change, ROUND( (revenue - LAG(revenue, 1) OVER (ORDER BY month)) / NULLIF(LAG(revenue, 1) OVER (ORDER BY month), 0) * 100, 2 ) AS mom_pct FROM monthly_revenue;

逻辑说明:LAG(revenue, 1)取上一行的 revenue,NULLIF防止除零。ORDER BY month保证按月份顺序取上一行。

参数说明:LAG(column, offset, default)的offset默认 1,default默认 NULL。同比通常用LAG(revenue, 12)取 12 个月前。LEAD()方向相反,取后 N 行。注意LAG/LEAD必须配合ORDER BY,否则行序不确定,结果随机。

3.4 去重取最新:ROW_NUMBER 的经典用法

按某个键去重、保留最新一条,是窗口函数最实用的场景之一。

-- 每个用户保留最新一条订单记录 WITH dedup AS ( SELECT user_id, order_id, order_time, amount, ROW_NUMBER() OVER ( PARTITION BY user_id ORDER BY order_time DESC ) AS rn FROM orders ) SELECT user_id, order_id, order_time, amount FROM dedup WHERE rn = 1;

逻辑说明:按user_id分区,order_time降序,最新一条的rn = 1。外层筛rn = 1即得每个用户的最新订单。

参数说明:ORDER BY列决定「最新」的定义。如果有多个时间相同的记录,ROW_NUMBER()会任意选一条,结果不稳定。需要稳定结果时,在ORDER BY里加次级排序键,比如ORDER BY order_time DESC, order_id DESC。

3.5 分组内百分比:SUM OVER 和 RATIO 计算

计算每个值在分组内的占比,用SUM() OVER (PARTITION BY ...)做分母。

-- 每个部门内各薪资等级的占比 SELECT dept_id, salary_level, COUNT(*) AS cnt, SUM(COUNT(*)) OVER (PARTITION BY dept_id) AS dept_total, ROUND( COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (PARTITION BY dept_id), 2 ) AS pct FROM employee_salary GROUP BY dept_id, salary_level;

逻辑说明:先按dept_id, salary_level聚合,再用SUM(COUNT(*)) OVER (PARTITION BY dept_id)算部门总数,两者相除得占比。

参数说明:窗口函数在GROUP BY之后执行,所以SUM(COUNT(*))里的COUNT(*)是聚合后的结果。注意COUNT(*) * 100.0里的100.0必须是浮点数,用100会整数除法截断。

4. 窗口函数避坑与排查:5 个血泪教训

4.1 默认窗口帧导致累计值偏大

现象:用SUM(amount) OVER (ORDER BY sale_date)算累计值,结果比预期大,尤其是同一天有多条记录时。

原因:有ORDER BY但没写窗口帧时,默认帧是RANGE UNBOUNDED PRECEDING TO CURRENT ROW,所有与当前行ORDER BY值相同的行会被一起纳入。同一天的多条记录会互相累加,导致重复计算。

解决:明确写ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW,强制按物理行累计。

4.2 PARTITION BY 漏列导致跨组串行

现象:按部门排名,结果不同部门的员工混在一起排名。

原因:PARTITION BY写漏了部门列,或者写成了其他列。窗口函数在不分区时对整个结果集计算。

解决:检查PARTITION BY是否包含了所有分组维度。可以用COUNT(*) OVER (PARTITION BY dept_id)验证分区大小是否符合预期。

4.3 ORDER BY 不唯一导致结果不稳定

现象:同样的 SQL 跑两次,ROW_NUMBER()的结果不一样。

原因:ORDER BY列有重复值,数据库无法确定相同值行的顺序,每次执行可能不同。

解决:在ORDER BY里加唯一键做次级排序,比如ORDER BY score DESC, id ASC。这样即使 score 相同,id 也能保证顺序稳定。

4.4 窗口函数在 WHERE 之后执行

现象:想用WHERE rn = 1过滤窗口函数结果,报错说rn不存在。

原因:窗口函数在WHERE、GROUP BY、HAVING之后执行,不能直接在WHERE里引用窗口函数别名。

解决:用 CTE 或子查询包一层,外层再过滤。这是 SQL 执行顺序决定的,不是语法问题。

4.5 RANGE 帧在 MySQL 中的限制

现象:在 MySQL 8.0 里写RANGE BETWEEN INTERVAL '2' DAY PRECEDING AND CURRENT ROW报错。

原因:MySQL 对RANGE帧的INTERVAL支持有限,只支持数值类型的偏移,不支持日期类型的INTERVAL。

解决:改用ROWS帧,或者把日期转成数值(如UNIX_TIMESTAMP)再用RANGE。PostgreSQL 对RANGE+INTERVAL支持较好,跨库迁移时要注意这个差异。

5. 窗口函数性能优化:从全表扫描到索引命中

窗口函数的性能瓶颈通常不在函数本身,而在排序和分区。PARTITION BY和ORDER BY都需要排序,如果数据量大且没有合适的索引,就会触发全表扫描加外部排序。我一般会先看执行计划里有没有Sort节点,如果有且代价很高,就考虑加索引。

-- 为窗口函数创建复合索引 CREATE INDEX idx_dept_salary ON employee_salary (dept_id, salary DESC); -- 验证执行计划 EXPLAIN ANALYZE SELECT dept_id, emp_name, salary, ROW_NUMBER() OVER ( PARTITION BY dept_id ORDER BY salary DESC ) AS rn FROM employee_salary;

逻辑说明:索引列顺序要和PARTITION BY+ORDER BY一致,这样数据库可以直接按索引顺序读取,避免额外排序。EXPLAIN ANALYZE看实际执行时间,对比加索引前后的差异。

参数说明:索引的DESC要和窗口ORDER BY的方向一致。如果窗口函数有多个不同的ORDER BY,可能需要多个索引。注意索引会增加写入开销,只在对查询性能要求高的场景加。

另一个优化方向是减少窗口函数的数据量。如果只需要对最近一个月的数据做排名,先用WHERE过滤再开窗,比全表开窗再过滤快得多。窗口函数本身不减少行数,它只是给每行附加计算值,所以能提前过滤就提前过滤。

提示:窗口函数和GROUP BY可以同时出现在一个查询里,但执行顺序是先GROUP BY再窗口函数。如果发现窗口函数的结果和预期不符,先检查GROUP BY是否改变了行数。

我自己的习惯是:写完窗口函数 SQL 后,先用小数据集验证结果,再上大数据集看执行计划。窗口函数的错误往往不是语法错误,而是语义错误——结果不报错,但就是不对。这种时候只能靠对比验证,没有后悔药可吃。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 10:03:40

计算机网络核心知识梳理:分层模型、IP计算与排障实战

很多刚接触计算机网络的人都有同感&#xff1a;协议名一堆&#xff0c;分层看了就忘&#xff0c;ping通了但网页还是打不开&#xff0c;抓包抓了也不懂看。这篇内容就是一次针对计算机网络核心知识体系的系统梳理&#xff0c;聚焦在网络到底怎么运转、IP和子网怎么算、TCP为什么…

作者头像 李华
网站建设 2026/10/9 10:03:23

Floyd算法详解:动态规划实现全源最短路径与常见坑位

算法基础篇写到第11篇&#xff0c;今天聊Floyd算法。很多刷题的朋友一开始接触最短路时&#xff0c;通常先学Dijkstra&#xff0c;等遇到多源最短路或者带负权边的图时才意识到&#xff0c;Floyd这套方案有多省心。Floyd-Warshall算法是一套基于动态规划的全源最短路径算法&…

作者头像 李华
网站建设 2026/10/9 10:03:07

为Claude Code打造持久记忆:SQLite与语义检索的本地记忆库实践

1. 没有记忆的Claude会话&#xff0c;正在逼你做无效劳动1.1 每次冷启动&#xff0c;都是同一件事的重复劳动如果你用过Claude Code这类跑在终端里的AI编程工具&#xff0c;大概率经历过下面这个场景&#xff1a;昨天还在跟Claude讨论某个服务的表结构设计&#xff0c;聊到一半…

作者头像 李华
网站建设 2026/10/9 10:02:52

MES系统解决方案:66页落地文档背后的设备集成与OEE治理实践

简介&#xff1a;本资源是一份66页完整的MES系统解决方案Word文档&#xff0c;面向制造业信息化工程师、生产系统实施顾问及智能制造项目负责人&#xff0c;聚焦解决计划层与执行层脱节、设备利用率低、生产异常响应滞后等车间管理痛点。方案以WIP在制品管理与SCADA设备联网为核…

作者头像 李华