Oracle ROWNUM 你所需要知道的一切

本文翻译自 https://blogs.oracle.com/oraclemagazine/on-rownum-and-limiting-results

一.Limiting Result Sets

ROWNUM是Oracle数据库一个神奇的栏目,让很多陷入麻烦。但是,当你学会了它是什么,它如何工作,ROWNUM可能会很有用。我使用它完成两件事:

1.执行TOP-N处理。这与其他数据库里的Limit语句功能是相似的。

2.通过查询来分页,尤其在一个无状态的环境中,比如说web。我在asktom.oracle.com网站上使用这个技术。

在我回顾一下ROWNUM如何工作之后,我会看一下这些用法。

二.HOW ROWNUM WORKS

ROWNUM 是一个虚列(不是一个真实列),可以在查询中使用。ROWNUM将会被福成成数字1,2,3,4,…, N, ,其中N是与ROWNUM一起使用的集合中的行数。一个ROWNUM值不会被永远赋值给一行(一条记录)。(这是一个常见的误解)。表中的一行(一条记录)没有对应的数字;你不能要求从表中拿出第五行-没有这样的事情。

同样令许多人困惑的是什么时候一个ROWNUM值被真正地赋值。ROWNUM值在通过查询的谓词阶段之后但在查询执行任何排序或聚合之前分配给行。此外,ROWNUM值仅在分配后递增,这就是以下查询永远不会返回行的原因:

select * 
  from t 
 where ROWNUM > 1;

由于表中的第一行不满足ROWNUM>1, 所以ROWNUM不会增长到2.因此,永远不会有ROWNUM 数值大于1的情况。
考虑一个带有以下结构的查询:

select ..., ROWNUM
  from t
 where <where clause>
 group by <columns>
having <having clause>
 order by <columns>;

上面的语句将按照下面的顺序执行:

  • FROM/WHERE 语句第一个执行
  • ROWNUM被分配并递增到FROM / WHERE子句的每个输出行。
    • SELECT 被应用
    • GROUP BY 被应用
    • HAVING 被应用
    • ORDER BY 被应用

这就是为什么以下形式的查询几乎肯定是一个错误:

select * 
  from emp 
 where ROWNUM <= 5 
 order by sal desc;

上面的查询语句本想的是查到前5个薪水最高的人-一个TOP N查询。然而查询将返回的是五个随机记录(查询恰好命中的前五个),
按工资排序。此查询的过程伪代码如下:

ROWNUM = 1
for x in 
( select * from emp )
loop
    exit when NOT(ROWNUM <= 5)
    OUTPUT record to temp
    ROWNUM = ROWNUM+1
end loop
SORT TEMP

它获得前五个记录,然后对它们进行排序。WHERE ROWNUM = 5或WHERE ROWNUM> 5的查询没有意义。这是因为在谓词评估期间将ROWNUM值分配给一行,并且只有在一行通过WHERE子句后才会增加。
以下是此查询的正确版本:

select *
  from  
( select * 
    from emp 
   order by sal desc ) 
 where ROWNUM <= 5;

此版本将按工资降序对EMP进行排序,然后返回它遇到的前五个记录(前五个记录)。正如您将在即将出现的前N个讨论中看到的那样,Oracle数据库并没有真正对整个结果集进行排序 - 它比这更聪明 - 但从概念上讲,这就是发生的事情。

三.Top- N Query Processing with ROWNUM (带有ROWNUM的Top-N查询处理)

在一个TOP-N查询中,你通常都想要执行一些复杂的查询,排序,然后才获取前N行(top-N行)。
ROWNUM有一个前N个优化,可以促进这种类型的查询。你可以使用ROWNUM来避免大型集合的排序。我将在概念上讨论它是如何做到的,然后再看一个例子。
假设你有以这种形式的查询:

select ... 
  from ... 
 where ... 
 order by columns;

假设此查询返回大量数据:数千,数十万或更多行。但是,您只对Top-N(比如前10或前100)感兴趣。有两种方法可以解决这个问题:

  • 让客户端应用程序运行该查询并仅获取前N行
  • 将该查询用作内联视图,并使用ROWNUM限制结果,如SELECT * FROM(your_query_here)WHERE ROWNUM <= N.

由于两个原因,第二种方法远远优于第一种方法。这两个原因中较小的一个是客户端需要较少的工作,因为数据库负责限制结果集。更重要的原因是数据库可以执行的特殊处理只能为您提供前N行。使用top-N查询意味着您已为数据库提供了额外信息。你告诉它,“我只对获得N行感兴趣;我永远不会考虑其余的”。现在,在你考虑排序 - 排序如何工作以及服务器需要做什么之前,这听起来并不太令人震惊。让我们通过示例查询来讨论这两种方法:

select * 
  from t 
 order by unindexed_column;

现在,假设T是一个大表,有超过一百万条记录,并且每条记录都是“胖”的,即100个或更多字节。 还假设UNINDEXED_COLUMN正如其名称所暗示的那样,是未被索引的列。 并假设您只想获得前10行。 Oracle数据库将执行以下操作:

  • 在T上运行全表扫描
  • 按UNINDEXED_COLUMN排序T. 这是一个完整的排序。
  • 应该会用完排序区域内存并需要将临时扩展区交换到磁盘。
  • 合并临时扩展区以获取请求时的前10个记录。
  • 完成后清理(释放)临时扩展区。

现在,这是产生很多I / O. Oracle数据库很可能将整个表复制到TEMP中并将其写出来,只是为了得到前10行……

接下来,让我们看一下Oracle数据库在概念上使用top-N查询可以做什么:

select *
  from 
(select * 
   from t 
  order by unindexed_column)
 where ROWNUM < :N;

在这种情况下,Oracle数据库将采取以下步骤:

  • 像以前一样在T上运行全表扫描(您无法避免此步骤)。
  • 在一个包含N个元素的数组中(这次可能在内存中),只排序:N行。

前N行将按排序顺序填充此行数组。 当获取N + 1行时,它将与数组中的最后一行进行比较。 如果它进入数组中的插槽N + 1,则会被抛出。 否则,它将被添加到此数组并进行排序,并丢弃其中一个现有行。 您的排序区域最多包含N行,因此您可以排序N行,而不是排序一百万行。

使用数组概念和仅排序N行这个看似很小的细节可以带来性能和资源使用方面的巨大收益。 排序10行所需的RAM要少于排序100万行所需的RAM(更不用说TEMP空间使用)。

使用下表T,您可以看到虽然两种方法都获得了相同的结果,但它们使用的资源数量完全不同:

create table t
as
select dbms_random.value(1,1000000) 
id, 
       rpad('*',40,'*' ) data
  from dual
connect by level <= 100000;
begin
dbms_stats.gather_table_stats
( user, 'T');
end;
/
Now enable tracing, via
exec 
dbms_monitor.session_trace_enable
(waits=>true);

然后使用ROWNUM运行您的top-N查询:

select *
  from
(select *
   from t
  order by id)
where rownum <= 10;

最后运行一个“自己动手”的查询,只获取前10条记录:

declare
cursor c is
select *
  from t
 order by id;
l_rec c%rowtype;
begin
    open c;
    for i in 1 .. 10
    loop
        fetch c into l_rec;
        exit when c%notfound;
    end loop;
    close c;
end;
/

执行此查询后,您可以使用TKPROF格式化生成的跟踪文件并查看发生的情况。
首先检查top-N查询,如清单1所示。
### 3.1 代码清单1:使用ROWNUM的Top-N查询
select *
  from
(select *
   from t
  order by id)
where rownum <= 10

call count cpu elapsed disk query current rows


Parse 1 0.00 0.00 0 0 0 0
Execute 1 0.00 0.00 0 0 0 0
Fetch 2 0.04 0.04 0 949 0 10


total 4 0.04 0.04 0 949 0 10

Rows Row Source Operation


10 COUNT STOPKEY (cr=949 pr=0 pw=0 time=46997 us)
10 VIEW (cr=949 pr=0 pw=0 time=46979 us)
10 SORT ORDER BY STOPKEY (cr=949 pr=0 pw=0 time=46961 us)
100000 TABLE ACCESS FULL T (cr=949 pr=0 pw=0 time=400066 us)

查询读取整个表(因为它必须),但通过使用SORT ORDER BY STOPKEY步骤,它能够将临时
空间的使用限制为仅10行。 注意最后的行源操作行 -
它显示查询总共执行了949个逻辑I / O(cr = 949),
没有执行物理读取或写入(pr = 0和pw = 0),
并且花费了400066百万分之一秒 (0.04秒)。
将其与清单2中所示的自己动手方法进行比较。

3.2 代码清单2:没有ROWNUM的自己动手查询

SELECT * FROM T ORDER BY ID

call count cpu elapsed disk query current rows


Parse 1 0.00 0.00 0 0 0 0
Execute 2 0.00 0.00 0 0 0 0
Fetch 10 0.35 0.40 155 949 6 10


total 13 0.36 0.40 155 949 6 10

Rows Row Source Operation


10 SORT ORDER BY (cr=949 pr=155 pw=891 time=401610 us)
100000 TABLE ACCESS FULL T (cr=949 pr=0 pw=0 time=400060 us)

经过的时间包括等待以下事件:
Event waited on Times


direct path write temp 33
direct path read temp 5
如您所见,这个结果非常不同。 值得注意的是,经过的时间/ CPU时间明显更高,并且最终的行源操作行提供了对其原因的深入了解。 您必须对磁盘执行排序,您可以使用pw = 891(物理写入)查看。 您的查询执行了一些直接路径读取和写入 - 100,000个记录(而不仅仅是我们最终感兴趣的10个)的记录发生在磁盘上 - 大大增加了查询的运行时/资源使用情况。

四.使用ROWNUM分页

我最喜欢使用ROWNUM的是分页。 在这种情况下,我使用ROWNUM来获取结果集的行N到M. 一般形式如下:

select * 
  from ( select /*+ FIRST_ROWS(n) */ 
  a.*, ROWNUM rnum 
      from ( your_query_goes_here, 
      with order by ) a 
      where ROWNUM <= 
      :MAX_ROW_TO_FETCH ) 
where rnum  >= :MIN_ROW_TO_FETCH;

其中:

  • FIRST_ROWS(N)告诉优化器,“嘿,我有兴趣获得第一行,我会尽可能快地获得N个。”

  • :MAX_ROW_TO_FETCH设置为结果集的最后一行以获取 - 如果您想要结果集的50到60行,则将其设置为60。

  • :MIN_ROW_TO_FETCH设置为要获取的结果集的第一行,因此要获取行50到60,您可以将其设置为50。

此方案背后的概念是具有Web浏览器的最终用户已完成搜索并正在等待结果。 必须尽快返回第一个结果页面(和第二页,依此类推)。 如果仔细查看该查询,您会注意到它包含一个前N个查询(从查询中获取第一个:MAX_ROW_TO_FETCH行),因此可以从我刚刚描述的前N个查询优化中获益。 此外,它仅通过网络向客户端返回感兴趣的特定行 - 它从结果集中删除任何不感兴趣的前导行。
使用此分页查询的一个重要事项是ORDER BY语句应按唯一的顺序排序。 如果您排序的东西不是唯一的,那么您应该在ORDER BY的末尾添加一些内容来实现它。 例如,如果您按SALARY排序100条记录,并且它们都具有相同的SALARY值,那么指定第20行到第25行实际上没有任何意义。 为了说明这一点,这里我们使用包含大量重复ID值的小表:

SQL> create table t
  2  as
  3  select mod(level,5) id, 
     trunc(dbms_random.value(1,100)) data 
  4    from dual
  5  connect by level <= 10000;
Table created.

然后在按ID列排序后查询行148到150和151:

SQL> select *
  2    from
  3  (select a.*, rownum rnum
  4     from
  5  (select id, data
  6     from t
  7   order by id) a
  8   where rownum <= 150
  9  )
 10   where rnum >= 148;

ID DATA RNUM


0 38 148
0 64 149
0 53 150

SQL>
SQL> select *
  2    from
  3  (select a.*, rownum rnum
  4     from
  5  (select id, data
  6     from t
  7   order by id) a
  8   where rownum <= 151
  9  )
 10   where rnum >= 148;

ID DATA RNUM


0 59 148
0 38 149
0 64 150
0 53 151
注意在这种情况下,对于行148的一次,结果返回DATA = 38,并且下一次,结果返回DATA = 59。 根据您的要求,两个查询都返回正确答案:按ID排序数据,抛出前147行,然后返回下3或4行。 他们两个都这样做,但由于ID有很多重复值,查询无法确定性地执行 - 从运行到查询运行不能保证相同的排序顺序。 为了纠正这个问题,您需要添加ORDER BY独有的内容。 在这种情况下,只需使用ROWID:

SQL> select *
  2    from
  3  (select a.*, rownum rnum
  4     from
  5  (select id, data
  6     from t
  7   order by id, rowid) a
  8   where rownum <= 150
  9  )
 10   where rnum >= 148;

ID DATA RNUM


0 45 148
0 99 149
0 41 150

SQL>
SQL> select *
  2    from
  3  (select a.*, rownum rnum
  4     from
  5  (select id, data
  6     from t
  7   order by id, rowid) a
  8   where rownum <= 151
  9  )
 10   where rnum >= 148;

ID DATA RNUM


0 45 148
0 99 149
0 41 150
0 45 151
现在查询非常具有确定性。 ROWID在表中是唯一的,因此如果您使用ORDER BY ID
然后在ID中使用ORDER BY ROWID,则行将具有明确的确定性顺序,并且分页查询
将确定性地按预期返回行。

五:ROWNUM 总结

我猜测你和许多其他读者现在对ROWNUM有了新的认识,并了解这些方面:
如何分配ROWNUM, 因而你可以编写使用ROWNUM的无错误查询
它如何影响查询的处理,因而你可以使用它在Web上对查询进行分页
它如何减少查询所执行的工作,以便曾经消耗大量TEMP空间的前N个查询,现在不需要额外的空间并且返回结果的速度更快。

注:虽然作者喜欢用ROWNUM分页,但注意这里面一定要走索引,否则导致深分页巨慢无比。