很多人第一次写“分组取最新记录”时,会直觉把 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
先缩小数据集,再做窗口函数,通常比一上来全表编号更现实。












