1. 程式人生 > >Oracle 查詢技巧與優化(二) 多表查詢

Oracle 查詢技巧與優化(二) 多表查詢

前言

上一篇blog介紹了Oracle中的單表查詢和排序的相關技巧(http://blog.csdn.net/wlwlwlwl015/article/details/52083588),本篇blog繼續介紹查詢中用的最多的——多表查詢的技巧與優化方式,下面依舊通過一次例子看一個最簡單的多表查詢。

多表查詢

上一篇中提到了學生資訊表的民族程式碼(mzdm_)這個欄位通常應該關聯字典表來查詢其對應的漢字,實際上我們也是這麼做的,首先簡單看一下表結構,首先是字典表:
這裡寫圖片描述

如上圖,可以看到每個民族程式碼和名稱都是由兩個欄位——“itemkey_”和“itemvalue_”以鍵值形式對應起來的,而學生資訊表只存了民族程式碼欄位(mzdm_),所以通過mzdm_和itemkey_相對應就能很好的查詢出民族對應的漢字了,比如這樣寫:

select t1.*, t2.itemvalue_ mzmc_
  from (select sid_, stuname_, mzdm_ from t_studentinfo) t1
  left join (select itemkey_, itemvalue_
               from t_dict
              where itemname_ = 'EthnicType') t2
    on t1.mzdm_ = t2.itemkey_;

接下來檢視一下執行結果:
這裡寫圖片描述

如上寫法(左連線查詢)是我在專案中運用最多的形式之一,暫不評論好壞與效率,總之查詢結果是很好的展現出來了,接下來就具體研究一下多表查詢的幾種方式與區別。

UNION ALL

如題,這是我們第一個介紹的操作多表的方式就是UNION和UNION ALL,UNION和UNION ALL也是存在一定區別的,首先明確一點基本概念,UNION和UNION ALL是用來合併多個數據集的,例如將兩個select語句的結果合併為一個整體:

select bmh_, stuname_, csrq_, mzdm_
  from t_studentinfo
 where mzdm_ = 2
union all
select bmh_, stuname_, csrq_, mzdm_
  from t_studentinfo
 where mzdm_ = 5

查詢結果如下:
這裡寫圖片描述

如上圖所示,把mzdm_為2和5的結果集合並在了一起,那麼接下來把UNION ALL換成UNION再看一下執行結果:
這裡寫圖片描述

注意觀察上圖中的第一列BMH_不難發現,UNION進行了排序(預設規則排序,即按查詢結果的首列進行排序),這就是它與UNION ALL的區別之一,再看一下下面這兩個SQL和查詢結果:

select bmh_, stuname_, csrq_, mzdm_
  from t_studentinfo
 where mzdm_ in (2, 5)
   and csrq_ like '200%';

執行結果如下:
這裡寫圖片描述

select bmh_, stuname_, csrq_, mzdm_
  from t_studentinfo
 where mzdm_ in (2, 5)
   and csrq_ like '2001%';

執行結果如下:
這裡寫圖片描述

可以看到第二段查詢結果肯定是包含在第一段查詢結果之內的,那麼它們進行UNION和UNION ALL又會有何區別呢?分別看一下,首先是UNION ALL:
這裡寫圖片描述

如上圖,不難發現使用UNION ALL查詢出了上面兩個結果集的總和,包括6對重複資料+5條單獨的資料總共17條,那麼再看看UNION的結果:
這裡寫圖片描述

顯而易見,和UNION ALL相比UNION幫我們自動剔除了6條重複結果,得到的是上面兩個結果集的並集,同時並沒有排序,這也就是UNION ALL與UNION的第二個區別了,最後簡單總結一下UNION與UNION ALL的區別:

  1. UNION會自動去除多個結果集合中的重複結果,而UNION ALL則將所有的結果全部顯示出來,不管是不是重複。
  2. UNION會對結果集進行預設規則的排序,而UNION ALL則不會進行任何排序。

所以效率方面很明顯UNION ALL要高於UNION,因為它少去了排序和去重的工作。當然還有一點需要注意,UNION和UNION ALL也可以用來合併不同的兩張表的結果集,但是欄位型別和個數需要匹配,例如:

select sid_, stuname_, mzdm_
  from t_studentinfo
 where sid_ = '33405'
union all
select did_, itemvalue_, itemkey_
  from t_dict
 where did_ = '366'

檢視一下執行結果:
這裡寫圖片描述

當資料配型不匹配或是列數不匹配時則會報錯:
這裡寫圖片描述
這裡寫圖片描述

當列數不夠時完全也可以用NULL來代替從而避免上圖中的錯誤。最後再舉個例子看一下UNION ALL在某些比較有意義的場景下的作用,首先建立一張臨時表:

with test as
 (select 'aaa' as name1, 'bbb' as name2
    from dual
  union all
  select 'bbb' as name1, 'ccc' as name2
    from dual
  union all
  select 'ccc' as name1, 'ddd' as name2
    from dual
  union all
  select 'ddd' as name1, 'eee' as name2
    from dual
  union all
  select 'eee' as name1, 'fff' as name2
    from dual
  union all
  select 'fff' as name1, 'ggg' as name2
    from dual)
select * from test;

執行結果如下:
這裡寫圖片描述

我們的需求也很簡單,即:統計NAME1和NAME2中每個不同的值出現的次數。談一下思路,首先統計NAME1每個值出現的次數,再統計NAME2每個值出現的次數,最後對上面兩個結果集進行UNION ALL合併,最後再進行一次分組和排序即可:

with test as
 (select 'aaa' as name1, 'bbb' as name2
    from dual
  union all
  select 'bbb' as name1, 'ccc' as name2
    from dual
  union all
  select 'ccc' as name1, 'ddd' as name2
    from dual
  union all
  select 'ddd' as name1, 'eee' as name2
    from dual
  union all
  select 'eee' as name1, 'fff' as name2
    from dual
  union all
  select 'fff' as name1, 'ggg' as name2
    from dual)
select namex, sum(times) times
  from (select name1 namex, count(*) times
          from test
         group by name1
        union all
        select name2 namex, count(*) times
          from test
         group by name2)
 group by namex
 order by namex;

執行結果如下:
這裡寫圖片描述

OK,很好的完成了查詢,那麼關於UNION和UNION ALL暫且介紹到這裡。

是否使用JOIN

如題,blog開頭寫的那個例子是使用LEFT JOIN完成兩張表的關聯查詢的,那麼另外也可以不用JOIN而通過WHERE條件來完成以達到相同的效果:

select t1.sid_, t1.stuname_, t1.mzdm_, t2.itemvalue_ mzmc_
  from t_studentinfo t1, t_dict t2
 where t1.mzdm_ = t2.itemkey_
   and t2.itemname_ = 'EthnicType';

執行效果如下:
這裡寫圖片描述

回頭看一下blog開頭的SQL和執行效果,可以發現和上圖一模一樣,那使用哪一種更合適呢?JOIN的寫法是SQL-92的標準,多表關聯時候使用JOIN方式進行關聯查詢可以更清楚的看到各表之間的聯絡,也方便維護SQL,所以還是不建議上面使用WHERE的查詢方式,而是應該使用JOIN的寫法。

IN和EXISTS

如題,這也是在查詢中經常用到的,尤其是IN關鍵字,在專案中使用的相當頻繁,經常會有通過for迴圈和StringBuffer來拼接IN語句的寫法,那麼接下來就仔細看一下IN和EXISTS的使用場景以及效率問題,依舊通過舉例說明,比如這個需求,查詢所有漢族學生的成績:

explain plan for select *
  from t_studentscore
 where bmh_ in (select bmh_ from t_studentinfo where mzdm_ = 1);
select * from table(dbms_xplan.display());

觀察一下執行計劃:

1 Plan hash value: 902966761
2
3 ————————————————————————————-
4 | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
5 ————————————————————————————-
6 | 0 | SELECT STATEMENT | | 535 | 37985 | 240 (1)| 00:00:03 |
7 |* 1 | HASH JOIN | | 535 | 37985 | 240 (1)| 00:00:03 |
8 |* 2 | TABLE ACCESS FULL| T_STUDENTINFO | 535 | 5885 | 207 (1)| 00:00:03 |
9 | 3 | TABLE ACCESS FULL| T_STUDENTSCORE | 11642 | 682K| 32 (0)| 00:00:01 |
10 ————————————————————————————-
11
12 Predicate Information (identified by operation id):
13 —————————————————
14
15 1 - access(“BMH_”=SYS_OP_C2C(“BMH_”))
16 2 - filter(“MZDM_”=1)

同理,將IN換成EXISTS再來看一下SQL和執行計劃:

explain plan for select *
  from t_studentscore ts
 where exists (select 1
          from t_studentinfo
         where mzdm_ = 1
           and bmh_ = ts.bmh_);
select * from table(dbms_xplan.display());

觀察一下執行計劃:

1 Plan hash value: 3857445149
2
3 —————————————————————————————
4 | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
5 —————————————————————————————
6 | 0 | SELECT STATEMENT | | 1 | 71 | 240 (1)| 00:00:03 |
7 |* 1 | HASH JOIN RIGHT SEMI| | 1 | 71 | 240 (1)| 00:00:03 |
8 |* 2 | TABLE ACCESS FULL | T_STUDENTINFO | 535 | 5885 | 207 (1)| 00:00:03 |
9 | 3 | TABLE ACCESS FULL | T_STUDENTSCORE | 11642 | 682K| 32 (0)| 00:00:01 |
10 —————————————————————————————
11
12 Predicate Information (identified by operation id):
13 —————————————————
14
15 1 - access(“TS”.”BMH_”=SYS_OP_C2C(“BMH_”))
16 2 - filter(“MZDM_”=1)

如上所示,儘管IN的寫法用了HASH JOIN(雜湊連線)而EXISTS的寫法用了HASH JOIN RIGHT SEMI(雜湊右半連線),但它們的執行計劃卻沒有區別,效率都是一樣的,這是因為資料量不大,所以有一點結論就是在簡單查詢中,IN和EXISTS是等價的。還有一點需要明確,在早期的版本中彷彿有這樣的規則:

  1. 子查詢結果集小,用IN。
  2. 外表小,子查詢表大,用EXISTS。

這兩個說法在Oracle11g中已經是完全錯誤的了!在Oracle8i中這樣也許還經常是正確的,但Oracle 9i CBO就已經優化了IN和EXISTS的區別,Oracle優化器有個查詢轉換器,很多SQL雖然寫法不同,但是Oracle優化器會根據既定規則進行查詢重寫,重寫為優化器覺得效率最高的SQL,所以可能SQL寫法不同,但是執行計劃卻是完全一樣的,所以還有個結論就是:關於IN和EXISTS哪種更高效應該及時檢視PLAN,而不是記固定的結論,至少在目前的Oracle版本中是這樣的。

INNER LEFT RIGHT FULL JOIN

如題,很常用的幾種連線方式,下面就分別看一下它們之間的區別。

INNER JOIN

首先是內連線(INNER JOIN),顧名思義,INNER JOIN返回的是兩表相匹配的資料,依舊以blog開頭的例子改寫為INNER JOIN:

select t1.sid_, t1.stuname_, t1.mzdm_, t2.itemvalue_ mzmc_
  from t_studentinfo t1
 inner join t_dict t2
    on t1.mzdm_ = t2.itemkey_
 where t2.itemname_ = 'EthnicType';

執行結果如下:
這裡寫圖片描述

可以看到和上面的結果依舊是完全一樣,但這個例子沒有說明INNER JOIN的特點,所以就再重新建立兩張表說明一下問題,這次用比較經典的學生表班級表來進行測試:

create table T_TEST_STU
(
  sid     INTEGER,
  stuname VARCHAR2(20),
  clsid   INTEGER
)
tablespace USERS
  pctfree 10
  initrans 1
  maxtrans 255
  storage
  (
    initial 64K
    next 1M
    minextents 1
    maxextents unlimited
  );
create table T_TEST_CLS
(
  cid   INTEGER,
  cname VARCHAR2(20)
)
tablespace USERS
  pctfree 10
  initrans 1
  maxtrans 255
  storage
  (
    initial 64K
    next 1M
    minextents 1
    maxextents unlimited
  );

表建立好後插入測試資料:

insert into T_TEST_STU (SID, STUNAME, CLSID) values (1, '張三', 1);

insert into T_TEST_STU (SID, STUNAME, CLSID) values (2, '李四', 1);

insert into T_TEST_STU (SID, STUNAME, CLSID) values (3, '小明', 2);

insert into T_TEST_STU (SID, STUNAME, CLSID) values (4, '小李', 3);

insert into T_TEST_CLS (CID, CNAME) values (1, '三年級1班');

insert into T_TEST_CLS (CID, CNAME) values (5, '三年級5班');

如上所示,可以看到非常簡單,學生表插入了4條資料,班級表插入了1條資料,用學生表的clsid來關聯班級表的cid查詢一下班級名稱,下面看一下使用INNER JOIN的查詢語句:

select t1.sid, t1.stuname, t2.cname
  from t_test_stu t1
 inner join t_test_cls t2
    on t1.clsid = t2.cid;

執行後可以看到查詢結果:
這裡寫圖片描述

如上所示,很好的驗證了INNER JOIN的概念,即返回兩表均匹配的資料,由於班級表只有1條1班的資料和1條5班的資料,而學生表僅有兩名1班的學生並且沒有任何5班的學生,所以自然只能返回兩條。

LEFT JOIN

如題,LEFT JOIN是以左表為主表,返回左表的全部資料,右表只返回相匹配的資料,將上面的SQL改為LEFT JOIN看一下:

select t1.sid, t1.stuname, t2.cname
  from t_test_stu t1
  left join t_test_cls t2
    on t1.clsid = t2.cid;

看一下執行結果:
這裡寫圖片描述

如上圖所示,也非常簡單,因為右表(班級表)並沒有2班和3班的資料,所以班級名稱不會顯示。

RIGHT JOIN

如題,RIGHT JOIN和LEFT JOIN是相反的,以右表資料為主表,左表僅返回相匹配的資料,同理將上面的SQL改寫為RIGHT JOIN的形式:

select t1.sid, t1.stuname, t2.cname
  from t_test_stu t1
 right join t_test_cls t2
    on t1.clsid = t2.cid;

執行結果如下:
這裡寫圖片描述

如上圖,由於是以班級表為主表進行關聯,所以匹配到1班的2名學生以及5班的資料。

FULL JOIN

如題,顧名思義,FULL JOIN就是不管左右兩邊是否匹配,一次性顯示出所有的查詢結果,相當於LEFT JOIN和RIGHT JOIN結果的並集,依舊將上面的SQL改寫為FULL JOIN並檢視結果:

select t1.sid, t1.stuname, t2.cname
  from t_test_stu t1
  full join t_test_cls t2
    on t1.clsid = t2.cid;

執行結果如下:
這裡寫圖片描述

到這裡這4種JOIN查詢方式就已經簡要的介紹完畢,單從概念上來將還是很好理解和區分的。

自關聯

如題,這是一個使用場景比較特殊的關聯方式,個人感覺如果資料庫合理設計的話不會出現這種需求吧,既然提到了就舉例說明一下,依舊以上面的測試學生表為例,現在需要新增一個欄位:

alter table T_TEST_STU add leader INTEGER;

假設有如下需求,每個學生都有一個直屬leader,負責檢查作業,老師為了避免作弊行為不會指定兩個人相互檢查,而是依次錯開,比如學生A檢查學生B,學生B檢查學生C,所以我們的表資料可以這樣來描述這個問題:
這裡寫圖片描述

如上圖,張三的LEADER是李四,李四的LEADER是小明,小明的LEADER是小李,而小李的LEADER又是張三,那麼問題來了,該如何查詢得到每個學生的LEADER的姓名呢?沒錯,這裡就用到了自關聯查詢,簡單的講就是把同一張表查兩遍並進行關聯,用檢視來說明獲取更清晰,所以首先建立兩個檢視:

CREATE OR REPLACE VIEW V_STU as select * from T_TEST_STU;
CREATE OR REPLACE VIEW V_LEADER as select * from T_TEST_STU;

接下來就通過自關聯查詢:

select v1.SID, v1.STUNAME, v1.CLSID, v1.LEADER, v2.STUNAME leader
  from V_STU v1
  left join V_LEADER v2
    on v1.LEADER = v2.SID
 order by v1.SID

執行結果如下:
這裡寫圖片描述

如上圖所示,這樣就通過自關聯很好的查詢出了每個學生對應的LEADER的姓名。

NOT IN和NOT EXISTS

如題,我們現在有一張學生資訊表和一張錄取結果表,例如我們想知道有哪些學生沒被錄取,即學生表有資料但錄取表卻沒有該學生的資料,這時就可以用到NOT IN或NOT EXISTS,依舊結合執行計劃看一看這種方式的差異:

explain plan for
select * from t_studentinfo where bmh_ not in (select bmh_ from t_lq);
select * from table(dbms_xplan.display());

觀察一下執行計劃:

1 Plan hash value: 4115710565
2
3 —————————————————————————————–
4 | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
5 —————————————————————————————–
6 | 0 | SELECT STATEMENT | | 119 | 52003 | 551 (1)| 00:00:07 |
7 |* 1 | HASH JOIN RIGHT ANTI NA| | 119 | 52003 | 551 (1)| 00:00:07 |
8 | 2 | TABLE ACCESS FULL | T_LQ | 11643 | 93144 | 343 (1)| 00:00:05 |
9 | 3 | TABLE ACCESS FULL | T_STUDENTINFO | 11772 | 4931K| 207 (1)| 00:00:03 |
10 —————————————————————————————–
11
12 Predicate Information (identified by operation id):
13 —————————————————
14
15 1 - access(“BMH_”=”BMH_”)

接下來將SQL轉換為NOT EXISTS再看一下執行計劃:

explain plan for 
select * from t_studentinfo t1 where not exists (select null from t_lq t2 where t1.bmh_ = t2.bmh_);
select * from table(dbms_xplan.display());

執行結果如下:

1 Plan hash value: 270337792
2
3 ————————————————————————————–
4 | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
5 ————————————————————————————–
6 | 0 | SELECT STATEMENT | | 119 | 52003 | 551 (1)| 00:00:07 |
7 |* 1 | HASH JOIN RIGHT ANTI| | 119 | 52003 | 551 (1)| 00:00:07 |
8 | 2 | TABLE ACCESS FULL | T_LQ | 11643 | 93144 | 343 (1)| 00:00:05 |
9 | 3 | TABLE ACCESS FULL | T_STUDENTINFO | 11772 | 4931K| 207 (1)| 00:00:03 |
10 ————————————————————————————–
11
12 Predicate Information (identified by operation id):
13 —————————————————
14
15 1 - access(“T1”.”BMH_”=”T2”.”BMH_”)

如上所示,兩個PLAN都應用了HASH JOIN RIGHT ANTI,所以它們的效率是一樣的,所以在Oracle11g中關於NOT IN和NOT EXISTS也沒有絕對的效率優劣,依舊是要通過PLAN來判斷和測試哪種更高效。

多表查詢時的空值處理

如題,假設有以下需求,我需要查詢一下性別不為男的學生的錄取分數,但在這之前我首先給學生表新增一條報名號(bmh_)為null的學生資料,如下所示:
這裡寫圖片描述

接下來寫查詢語句,這裡刻意用一下NOT IN關鍵字而不是IN關鍵字:

select bmh_, stuname_, lqfs_
  from t_lq
 where bmh_ not in (select bmh_ from t_studentinfo where sextype_ = 1)

執行結果如下圖所示:
這裡寫圖片描述

我們驚奇的發現沒有任何資料被查出來,這就是因為NOT IN後的子查詢中的5000+結果中僅僅有一條存在NULL值,所以這個查詢整體就不會顯示任何結果,有點一隻老鼠毀了一鍋湯的感覺,這也正是Oracle的特性之一,即:如果NOT IN關鍵字後的子查詢包含空值,則整體查詢都會返回空,所以這類查詢務必要加非NULL判斷條件,即:

select bmh_, stuname_, lqfs_
  from t_lq
 where bmh_ not in (select bmh_
                      from t_studentinfo
                     where sextype_ = 1
                       and bmh_ is not null);

這次再看一下執行結果:
這裡寫圖片描述

如上圖所示,這次就很好的查詢出了我們需要的結果。

總結

簡單記錄一下Oracle多表查詢中的各種模式以及個人認為值得注意的一些點和優化方式,希望對讀到的同學有所幫助和提高,The End。