窗口函数是SQL标准中非常重要的一类函数,DB2从9.5版本开始就已经全面支持。与普通聚合函数不同,窗口函数不会把多行合并成一行,而是在保留每一行明细数据的基础上,为每行计算出一个额外的结果值。这个特性使得它在排名、去重、累计求和等场景中几乎是不可替代的方案。本文围绕ROW_NUMBER和RANK这两个最常用的窗口函数,结合几个典型的业务案例,讲解它们的具体用法和容易踩的坑。

窗口函数的基本语法与执行逻辑
在DB2中,窗口函数的标准写法是在函数名后面跟上OVER子句,OVER子句内部定义分区和排序规则。基本结构如下:
SELECT
emp_name,
dept_id,
salary,
ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn
FROM employee;这段SQL的含义是:先按照dept_id将数据分成若干个分区,每个分区内再按salary从高到低排序,然后为分区内的每一行分配一个从1开始的连续编号。PARTITION BY是可选的,如果省略,整个结果集会被当作一个大的分区来处理。ORDER BY在ROW_NUMBER中是必须的,因为数据库必须知道按什么顺序编号,否则生成的编号就没有确定的含义。
需要特别理解的一点是,窗口函数的执行时机在WHERE、GROUP BY、HAVING之后,在ORDER BY和FETCH FIRST之前。这意味着你不能直接在WHERE子句中引用窗口函数的结果,比如写WHERE ROW_NUMBER() OVER(...) = 1在DB2中会报错。正确做法是先用子查询或者CTE生成编号,再在外层进行过滤。这个执行顺序是很多初学者犯错的高频区域,后面案例中会反复体现。
案例一:用ROW_NUMBER实现分组去重
去重是窗口函数最常见的应用场景之一。假设有一张订单变更记录表order_log,同一个订单号可能存在多条变更记录,业务要求只取每个订单最新的一条。传统做法是用关联子查询判断时间戳最大值,数据量大时性能很差。用ROW_NUMBER改写如下:
WITH ranked AS (
SELECT
order_id,
status,
update_time,
ROW_NUMBER() OVER (
PARTITION BY order_id
ORDER BY update_time DESC
) AS rn
FROM order_log
)
SELECT order_id, status, update_time
FROM ranked
WHERE rn = 1;这里的思路很清晰:按订单号分区,按更新时间倒序排列,编号为1的就是最新记录。相比关联子查询,这种写法只需要对表扫描一次,DB2优化器能够对窗口函数做高效的排序处理,在百万级数据量下性能差距非常明显。
有一个细节值得注意:如果同一订单存在两条update_time完全相同的记录,ROW_NUMBER会随机给其中一条编号1,结果不稳定。如果业务上需要确定性的结果,应该在ORDER BY中补充一个唯一的辅助字段,比如主键id,写成ORDER BY update_time DESC, id DESC,这样每次执行的结果都一致。这类偶发的结果不一致问题在生产环境排查起来非常困难,建议一开始就养成补齐排序字段的习惯。
另外提醒一点,如果你的目的只是单纯的整行去重(所有列都相同),用DISTINCT或者GROUP BY就够了,不需要动用窗口函数。ROW_NUMBER的价值在于按照特定业务规则保留某一条,两者不要混淆。
案例二:RANK与DENSE_RANK处理并列排名
ROW_NUMBER生成的编号永远是1、2、3、4这样连续的,即使两行的排序值相同,也会被强制分配不同的编号。但在很多排名场景中,相同分数应该得到相同名次,这时就需要RANK登场。先看三者的区别,用一张成绩表来演示:
SELECT
student_name,
score,
ROW_NUMBER() OVER (ORDER BY score DESC) AS row_num,
RANK() OVER (ORDER BY score DESC) AS rnk,
DENSE_RANK() OVER (ORDER BY score DESC) AS dense_rnk
FROM student_score;假设有三个学生都是95分,两个学生是90分,那么三列的结果分别是:ROW_NUMBER给出1、2、3、4、5这种连续不重复的编号;RANK给三个95分都编为1,后面两个90分则直接跳到4,因为前面实际占了三个位置;DENSE_RANK同样给三个95分编为1,但两个90分是2,名次是连续压缩的。简单总结就是:RANK跳跃、DENSE_RANK连续、ROW_NUMBER不并列。
在实际业务中,这三者的选择取决于需求。体育比赛排名通常用RANK,比如两人并列冠军,下一名就是第三名;计算薪酬分档或者绩效等级时往往用DENSE_RANK,因为档位数量本身是有限的;而纯粹为了去重或者取前N条,ROW_NUMBER就够了。
再补充一个组合用法:如果要取每个部门工资排名前二的员工,可以把PARTITION BY和RANK结合起来:
WITH ranked AS (
SELECT
emp_name,
dept_id,
salary,
RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rnk
FROM employee
)
SELECT emp_name, dept_id, salary
FROM ranked
WHERE rnk <= 2;注意这里如果用ROW_NUMBER,遇到并列第二的员工就只会保留一个,可能不符合业务预期。如果希望并列的都保留,RANK或者DENSE_RANK才是正确选择。可见函数选型本身就是一个业务判断,不是单纯的技术问题。
常见错误与性能优化建议
第一个高频错误是遗漏ORDER BY。在DB2中,ROW_NUMBER的OVER子句如果不写ORDER BY,语法上是允许的,此时编号按照系统返回的任意顺序分配,结果不可预测。有些开发者误以为不写ORDER BY就等于随机取一条,实际上这个随机既不均匀也不可复现,用它做随机抽样是不可靠的,随机需求应该使用RAND函数配合排序来实现。
第二个问题是分区字段选择错误。比如按部门分组排名时误把公司级字段放进PARTITION BY,或者反过来漏掉了分组维度,导致排名结果跨组混乱。建议写完SQL后先用小数据量验证,确认每个分区内的编号都从1开始重新计数,再放到生产环境执行。
性能方面,窗口函数本质上依赖排序操作,所以PARTITION BY和ORDER BY涉及的列上如果有合适的索引,DB2可以直接利用索引避免显式排序,性能会好很多。对于频繁执行的分组取最新记录这类查询,可以考虑在分区列加排序列上建立复合索引。此外,尽量把不需要参与窗口计算的行先在WHERE中过滤掉,减少进入排序的数据量,这比事后在外层过滤编号要高效得多。
最后一点关于兼容性:ROW_NUMBER、RANK、DENSE_RANK都是SQL标准函数,在DB2、Oracle、SQL Server、MySQL 8.0中写法基本一致,掌握之后可以平滑迁移到其他数据库。但一些扩展窗口函数如LISTAGG OVER的细节在各平台存在差异,跨库迁移时需要逐一验证。熟练运用窗口函数后,你会发现过去很多用自关联、多层子查询实现的复杂逻辑,都可以用一条清晰的窗口函数语句重写,代码可读性和执行效率都会有明显提升。
DB2窗口函数ROW_NUMBERRANK修改时间:2026-09-13 04:30:30