Oracle中的SQL分頁查詢原理和方法詳解

不一樣的天空w發表於2017-05-23

Oracle中的SQL分頁查詢原理和方法詳解

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

本文分析並介紹中的分頁查詢的方法。

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


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31397003/viewspace-2139766/,如需轉載,請註明出處,否則將追究法律責任。

相關文章