Oracle 21c 中怎么用 ROW_NUMBER() 查询分组最新记录?

陈默
陈默
发布时间:2026-08-24 | 80人浏览过
原创
窗口函数查询的索引匹配与大分组性能关注点示意图
索引设计与大分组性能风险把索引字段顺序、函数索引和大分组预过滤放在一张图里,帮助判断为什么同样写法性能差异会很大。
ROW_NUMBER、RANK 与 DENSE_RANK 处理并列记录的差异对比图
并列最新时三种编号函数怎么选对比三种窗口函数在并列值场景下的编号方式,方便判断是保留一条还是保留全部并列记录。
分组取最新记录的窗口函数执行流程图
ROW_NUMBER() 取每组最新记录的四用分组、倒序排序、编号和外层过滤四步展示 ROW_NUMBER() 查询每组最新记录的标准写法。

前言

在 Oracle 21c 里,想查“每组最新一条记录”,真正决定结果的不是单独的 ROW_NUMBER(),而是窗口里的分组与排序方式。下面按报错原因、正确 SQL、并列处理和索引设计四部分拆开讲,方便你判断什么时候该用 ROW_NUMBER(),什么时候该换成 RANK() 或先做预过滤。

很多人第一次写“分组取最新记录”时,会直觉把 ROW_NUMBER() 和 GROUP BY 放在一起,但在 Oracle 21c 里这条路走不通。真正可用的写法,是先在明细行上完成分组排序和编号,再到外层筛出每组第 1 条;同时还要看清并列值、NULL 和索引是否会影响结果与性能。

ROW_NUMBER() 能做什么,关键卡在排序

ROW_NUMBER() 本身并不负责“找最新”,它只是在窗口内按指定顺序给每一行编号。要拿到每组最新记录,决定性因素是 OVER 里的 ORDER BY:必须使用能够体现新旧关系的字段,并按倒序排列,例如时间戳、版本号或自增 ID。

也就是说,查询逻辑其实是两步:

  • 先按分组键划分窗口;
  • 再按“越新越靠前”的字段 DESC 排序,最后取编号为 1 的记录。

为什么不能直接和 GROUP BY 一起写

ROW_NUMBER() 是分析函数,作用对象是未聚合的明细行;而 GROUP BY 会先把多行聚合成一行。两者的语义不在同一层,直接混用通常会报错 ORA-00979: not a GROUP BY expression。

常见错误写法如下:

SELECT deptno, MAX(sal), ROW_NUMBER() OVER (PARTITION BY deptno ORDER BY sal DESC) FROM emp GROUP BY deptno;

这条 SQL 的问题在于:MAX(sal) 已经把每个部门压成一行,但 ROW_NUMBER() 仍然需要针对原始明细逐行编号,所以 Oracle 无法按这个语义执行。

正确写法:子查询编号,外层取 rn = 1

可行方案是把分析函数放进子查询,先在每组内部完成编号,再从外层取出 rn = 1。

假设表 orders 有字段 customer_id、order_date、amount,要查询每个客户最新一笔订单,逻辑如下:

  • 按 customer_id 分组;
  • 按 order_date DESC 排序,让最新记录排在最前;
  • 用 ROW_NUMBER() 生成序号;
  • 外层过滤 rn = 1。

完整 SQL:

SELECT customer_id, order_date, amount
FROM (
SELECT customer_id, order_date, amount,
 ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_date DESC) AS rn
FROM orders
)
WHERE rn = 1;

如果你的“最新”不是按时间判断,而是按版本号、流水号或业务状态判断,只需要把 ORDER BY 换成对应字段,并保持“最新优先”的排序方向即可。

遇到并列最新记录,该用 RANK() 还是 DENSE_RANK()

ROW_NUMBER() 的特点是无论排序值是否相同,都会强制给出不同编号,例如 1,2,3。这意味着当多条记录在业务上同样“最新”时,它只会保留其中一条。

如果你的需求是“保留所有并列最新记录”,就不该继续用 ROW_NUMBER(),而应改用下面两个函数:

  • RANK() OVER (PARTITION BY ... ORDER BY ... DESC):并列时跳号,例如 1,1,3;
  • DENSE_RANK() OVER (PARTITION BY ... ORDER BY ... DESC):并列时不跳号,例如 1,1,2。

例如查询每个部门薪资最高的员工,并允许多人并列第一:

SELECT deptno, empno, sal
FROM (
SELECT deptno, empno, sal,
 RANK() OVER (PARTITION BY deptno ORDER BY sal DESC) AS rnk
FROM emp
)
WHERE rnk = 1;

至于 NULL 的处理,也要提前确认业务规则。若排序字段可能为空,实际结果会受排序规则影响,因此最好明确 NULL 是否应当被视为“最旧”或直接在查询前排除。

性能关注点:索引顺序和数据分布同样重要

这类查询的成本主要在窗口排序。是否高效,首先看 OVER 子句中的分组字段和排序字段能否被合适的索引支持。

  • 如果写法是 PARTITION BY deptno ORDER BY order_date DESC,理想索引就是 (deptno, order_date DESC);
  • 复合索引字段顺序应与窗口定义保持一致;
  • 若排序字段是表达式,例如 TRUNC(create_time),则需要考虑函数索引;
  • Oracle 21c 虽支持 WINDOW 子句复用,但对单个 ROW_NUMBER() 查询帮助有限,关键仍是索引设计。

还有一个容易忽略的问题,是数据分布不均。假如某个客户有 50 万条订单,即便有索引,数据库仍可能需要扫描该分组内大量记录来完成编号。此时可以重新评估需求:是否必须拿“绝对最新”一条,或者能否先做时间范围预过滤,例如:

WHERE order_date > SYSDATE - 30

先缩小数据集,再做窗口函数,通常比一上来全表编号更现实。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表