Oracle中的SQL分页查询原理和方法详解

Oracle中的SQL分页查询原理和方法详解

http://blog.csdn.net/anxpp/article/details/51534006
http://blog.csdn.net/honey_potter/article/details/53014284

本文分析并介绍Oracle中的分页查找的方法。

Oracle中的表,除了我们建表时设计的各个字段,其实还有两个字段(此处只介绍2个),分别是ROWID(行标示符)和ROWNUM(行号),即使我们使用DESCRIBE命令查看表的结构,也是看不到这两个列的描述的,因为,他们其实是只在数据库内部使用的,所以也通常称他们为伪列(pseudo column)。


下面我们先建表并添加一些数据来验证上面的说明。

建表:
create table users(
id integer primary key,
name nvarchar2(20)
)

插入数据:
insert into users(id,name) values(1,'tom');
insert into users(id,name) values(2,'cat');
insert into users(id,name) values(3,'bob');
insert into users(id,name) values(4,'anxpp');
insert into users(id,name) values(5,'ez');
insert into users(id,name) values(6,'lily');

使用describe命令查看表结构:
 desc users;
 名称                                                              是否为空? 类型
 --------------------------------------------------------------------------------------------
 ID                                                                NOT NULL NUMBER(38)
 NAME                                                               NVARCHAR2(20)

可以看到,确实只有建表时的两个字段。

但我们可以查询的时候,查找到伪列的值:
select rowid,rownum,id,name from users;

结果:
SQL> select rowid,rownum,id,name from users;

ROWID                  ROWNUM         ID NAME
------------------ ---------- ---------- ----------------------------------------
AAAV1mAAEAAAAKPAAA          1          1 tom
AAAV1mAAEAAAAKPAAB          2          2 cat
AAAV1mAAEAAAAKPAAC          3          3 bob
AAAV1mAAEAAAAKPAAD          4          4 anxpp
AAAV1mAAEAAAAKPAAE          5          5 ez


这个rowid我们一般用不到,Oracle数据库内部使用它来存储行的物理位置,是一个18位的数字,采用base-64编码。
而这个rownum,我们也正是使用它来进行分页查询的,它的值,就是表示的该行的行号。

对于分页,我们只要想办法可以查询到从某一起始行到终止行就可以的,分页的逻辑可以放到程序里面。
于是,我们理所当然会想到如下语句查询第2页的数据(每页2条数据,页码从1开始,所以起始行的行号为 (页码-1)*每页长度+1=3,终止行的行号为 页码*每页长度=4):

select * from users where rownum>=3 rownum <= 4;

SQL> select * from users where rownum>=3 rownum <= 4;
select * from users where rownum>=3 rownum <= 4
                                    *
第 1 行出现错误:
ORA-00933: SQL 命令未正确结束

哈哈!是不是发现没有任何结果,原因很简单,Oracle机制就是这样的:因为第一条数据行号为1,不符合>=3的条件,所以第一行被去掉,之前的第二行变为新的第一行(即这个行号不是写死的,可以理解为是动态的),如此下去,一直到最后一行,条件始终没法满足,所以就一条数据也查不出来。

既然找到了原因,解决方法也就很明显了,我们只要将行号查询出来生成一个结果集,然后再从这个结果集中,选择行号大于我们设定的那个值就可以了,上面的分页查找正确的写法应该是这样:

select id,name from(
       select rownum rn,u.* from users u) ua
where ua.rn between 3 and 4;

执行结果:
SQL> select rownum rn,u.* from users u;

        RN         ID NAME
---------- ---------- ----------------------------------------
         1          1 tom
         2          2 cat
         3          3 bob
         4          4 anxpp
         5          5 ez

SQL> select id,name from(
  2         select rownum rn,u.* from users u) ua
  3  where ua.rn between 3 and 4;

        ID NAME
---------- ----------------------------------------
         3 bob
         4 anxpp


上面的语句还可以优化:>=不能用,但是<=是可以的,我们不需要在子查询中将结果全部查出来,首先使用终止行筛选子查询的结果,SQL如下:

select id,name from(
       select rownum rn,u.* from users u where rownum<=4) ua
where ua.rn >= 3;

结果:
SQL> select rownum rn,u.* from users u where rownum<=4;

        RN         ID NAME
---------- ---------- ----------------------------------------
         1          1 tom
         2          2 cat
         3          3 bob
         4          4 anxpp

SQL> select id,name from(
  2         select rownum rn,u.* from users u where rownum<=4) ua
  3  where ua.rn >= 3;

        ID NAME
---------- ----------------------------------------
         3 bob
         4 anxpp

很多时候,我们并不是盲目的分页查找的,而是按某一个或多个字段的升序或降序分页,即包含order by语句的分页查询,我们先看一下 order by 的查询结果中rownum是怎样的:

select rownum,id,name from users order by name;

结果:
SQL> select rownum,id,name from users order by name;

    ROWNUM         ID NAME
---------- ---------- ----------------------------------------
         4          4 anxpp
         3          3 bob
         2          2 cat
         5          5 ez
         1          1 tom

可以看到,我们说行号完全是动态的,也是不准确的,这时候的行号并不是经过 order by 后新结果的增序行号。
但有了上面的嵌套查询的经验,这里也可以好好应用一下,怎么做呢:先查找出排序好的结果集,然后应用上面的方法得到最终结果,sql如下:

select id,name from(
       (select rownum rn,uo.* from
              (select * from users u order by name) uo
       where rownum<=4)) ua
where ua.rn>=3;

按照上面的结果,正确的分页结果应该是id为2和5的,看下结果:
SQL> select * from users u order by name;

        ID NAME
---------- ----------------------------------------
         4 anxpp
         3 bob
         2 cat
         5 ez
         1 tom

SQL> select rownum rn,uo.* from
  2  (select * from users u order by name) uo
  3  where rownum<=4;

        RN         ID NAME
---------- ---------- ----------------------------------------
         1          4 anxpp
         2          3 bob
         3          2 cat
         4          5 ez
         
SQL> select id,name from(
  2         (select rownum rn,uo.* from
  3                (select * from users u order by name) uo
  4         where rownum<=4)) ua
  5  where ua.rn>=3;

        ID NAME
---------- ----------------------------------------
         2 cat
         5 ez

OK,结果正确。

其实连表查询之类的,也是差不多的,多点嵌套而已,掌握了原理,随便分析一下就能写出对应的SQL了,而编写SQL时,我们也得动动脑子,毕竟SQL也是由优劣之分的。

=========补充ORACLE分页查询SQL语法——最高效的分页 ===============================
--1:无order by排序的写法。(效率最高)
--(经过测试,此方法成本最低,只嵌套一层,速度最快!即使查询的数据量再大,也几乎不受影响,速度依然!)
select *
  from (select rownum as rowno, t.*
          from emp t
         where hire_date between to_date ('20060501', 'yyyymmdd')
                             and to_date ('20060731', 'yyyymmdd')
           and rownum <= 20) table_alias
 where table_alias.rowno >= 10;

--2:有order by排序的写法。(效率最高)
--(经过测试,此方法随着查询范围的扩大,速度也会越来越慢哦!)
select *
  from (select tt.*, rownum as rowno
          from (  select t.*
                    from emp t
                   where hire_date between to_date ('20060501', 'yyyymmdd')
                                       and to_date ('20060731', 'yyyymmdd')
                order by create_time desc, emp_no) tt
         where rownum <= 20) table_alias
 where table_alias.rowno >= 10;

=================================================================================

=======================垃圾但又似乎很常用的分页写法==========================

=================================================================================
--3:无order by排序的写法。(建议使用方法1代替)
--(此方法随着查询数据量的扩张,速度会越来越慢哦!)
select *
  from (select rownum as rowno, t.*
          from k_task t
         where flight_date between to_date ('20060501', 'yyyymmdd')
                               and to_date ('20060731', 'yyyymmdd')) table_alias
 where table_alias.rowno <= 20 and table_alias.rowno >= 10;
--table_alias.rowno  between 10 and 100;


--4:有order by排序的写法.(建议使用方法2代替)
--(此方法随着查询范围的扩大,速度会越来越慢哦!)
select *
  from (select tt.*, rownum as rowno
          from (  select *
                    from k_task t
                   where flight_date between to_date ('20060501', 'yyyymmdd')
                                         and to_date ('20060531', 'yyyymmdd')
                order by fact_up_time, flight_no) tt) table_alias
 where table_alias.rowno between 10 and 20;

--5另类语法。(有order by写法)
--(语法风格与传统的sql语法不同,不方便阅读与理解,为规范与统一标准,不推荐使用。)
with partdata as
     (
        select rownum as rowno, tt.*
          from (  select *
                    from k_task t
                   where flight_date between to_date ('20060501', 'yyyymmdd')
                                         and to_date ('20060531', 'yyyymmdd')
                order by fact_up_time, flight_no) tt
         where rownum <= 20)
select *
  from partdata
 where rowno >= 10;

--6另类语法 。(无order by写法)
with partdata as
     (
        select rownum as rowno, t.*
          from k_task t
         where flight_date between to_date ('20060501', 'yyyymmdd')
                               and to_date ('20060531', 'yyyymmdd')
           and rownum <= 20)
select *
  from partdata
 where rowno >= 10;


yangtingkun分析:
---from :http://yangtingkun.itpub.net/post/468/100278

oracle的分页查询语句基本上可以按照本文给出的格式来进行套用。
分页查询格式:
select *
  from (select a.*, rownum rn
          from (select *
                  from table_name) a
         where rownum <= 40)
 where rn >= 21

其中最内层的查询select * from table_name表示不进行翻页的原始查询语句。rownum <= 40和rn >= 21控制分页查询的每页的范围。
上面给出的这个分页查询语句,在大多数情况拥有较高的效率。分页的目的就是控制输出结果集大小,将结果尽快的返回。在上面的分页查询语句中,这种考虑主要体现在where rownum <= 40这句上。
选择第21到40条记录存在两种方法,一种是上面例子中展示的在查询的第二层通过rownum <= 40来控制最大值,在查询的最外层控制最小值。而另一种方式是去掉查询第二层的where rownum <= 40语句,在查询的最外层控制分页的最小值和最大值。这是,查询语句如下:

select *
  from (select a.*, rownum rn
          from (select *
                  from table_name) a)
 where rn between 21 and 40
 
对比这两种写法,绝大多数的情况下,第一个查询的效率比第二个高得多。
这是由于cbo优化模式下,oracle可以将外层的查询条件推到内层查询中,以提高内层查询的执行效率。对于第一个查询语句,第二层的查询条件where rownum <= 40就可以被oracle推入到内层查询中,这样oracle查询的结果一旦超过了rownum限制条件,就终止查询将结果返回了。
而第二个查询语句,由于查询条件between 21 and 40是存在于查询的第三层,而oracle无法将第三层的查询条件推到最内层(即使推到最内层也没有意义,因为最内层查询不知道rn代表什么)。因此,对于第二个查询语句,oracle最内层返回给中间层的是所有满足条件的数据,而中间层返回给最外层的也是所有数据。数据的过滤在最外层完成,显然这个效率要比第一个查询低得多。
上面分析的查询不仅仅是针对单表的简单查询,对于最内层查询是复杂的多表联合查询或最内层查询包含排序的情况一样有效。

这里就不对包含排序的查询进行说明了,下一篇文章会通过例子来详细说明。
下面简单讨论一下多表联合的情况。
对于最常见的等值表连接查询,cbo一般可能会采用两种连接方式nested loop和hash join(merge join效率比hash join效率低,一般cbo不会考虑)。在这里,由于使用了分页,因此指定了一个返回的最大记录数,nested loop在返回记录数超过最大值时可以马上停止并将结果返回给中间层,而hash join必须处理完所有结果集(merge join也是)。那么在大部分的情况下,对于分页查询选择nested loop作为查询的连接方法具有较高的效率(分页查询的时候绝大部分的情况是查询前几页的数据,越靠后面的页数访问几率越小)。
因此,如果不介意在系统中使用hint的话,可以将分页的查询语句改写为:

select *
  from (select a.*, rownum rn
          from (select *
                  from table_name) a
         where rownum <= 40)
 where rn >= 21


请使用浏览器的分享功能分享到微信等