表的聯結方法

1.聯結方法

如果再你查詢中有多張表,在優化器確定了每個表最恰當的訪問方法之後,下一步就是確定將這些表聯結起來的最佳方法以及最恰當的順序。任何時候當在FROM子句中有多個表時,就需要進行聯結。表之間的關係通過where子句中的一個條件來定義。如果沒有指定任何條件,聯結就會隱含地定義爲一個表中每一行將與另一個表的所有行匹配,笛卡聯結。

聯結髮生在一對錶或者數據行源之間。當FORM子句存在多張表時,優化器將會決定哪種聯結運算對於每一對錶來說效率最高。聯結的方法有:嵌套循環聯結,散列聯結,排序-合併聯結和笛卡兒聯結。

1.1  嵌套循環聯結

嵌套循環聯結使用一次訪問運算所得到的結果集中的每一行與另一個表進行對碰,如果結果集大小是有限的並且在用來聯結的列上建有索引的話,這種聯結的效率通常是最高的,嵌套循環聯結的運算成本主要是讀取外層表中每一行並將其與所匹配的內存表中的行聯結所需的成本。

嵌套循環聯結就是一個循環嵌在另一個循環當中。外層循環基本上來說就是一個只使用where子句中屬於驅動表的條件對它進行查詢。當數據行進過外層條件篩選並確認匹配條件後,這些行爲就會逐個進入到內存循環中,然後再基於聯結列進行逐行查詢看是否與被聯結的表中的某一行匹配。如果這個一行與第二行相匹配,就會將被傳遞到查詢計劃的下一步或者如果沒有更多步驟的話直接被包含在最終結果集中。

這種類型的聯結的強大之處在於所使用的內存非常少的。因爲數據行集一次加工一行,所需要的開支也是非常小的。由於這個原因,除了使用一次加工一行這種方式來建立一個很大的數據集需要較長時間這一點以外,他也是適合進行大數據集加工的。這就是爲什麼前面提到嵌套循環聯結在結果集較小的時候是最好的。嵌套聯結的基本度量就是爲了準備最終結果集所需要訪問的數據塊數目。

1.2 排序--合併聯結

排序-合併聯結獨立地讀取需要聯結的兩張表,對每張表中的數據行(僅是那些滿足where子句中條件的數據行)按照聯結鍵進行排序,然後對排序後的數據行集進行合併。對這種聯結方法來說排序的開銷是非常大的。對於不能夠放入內存中的大數據來說,可能會使用臨時磁盤空間來排序。這是非常消耗內存和是時間資源的。但是一旦數據行排序完成了,合併過程非常快的。爲了進行合併,數據庫輪流操作兩個列表。比較最上面的數據行,丟棄在排序列中比另一列表的最上面一行出現得早的數據行,並只返回匹配的行。

SQL> select empno,ename,dname,loc from emp a,dept b where a.deptno=b.deptno;
Execution Plan
----------------------------------------------------------
Plan hash value: 844388907
----------------------------------------------------------------------------------------
| Id  | Operation     | Name    | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT     |       |    14 |   462 |     6 (17)| 00:00:01 |
|   1 |  MERGE JOIN     |       |    14 |   462 |     6 (17)| 00:00:01 |
|   2 |   TABLE ACCESS BY INDEX ROWID| DEPT    |     4 |    80 |     2 (0)| 00:00:01 |
|   3 |    INDEX FULL SCAN     | PK_DEPT |     4 |       |     1 (0)| 00:00:01 |
|*  4 |   SORT JOIN     |       |    14 |   182 |     4 (25)| 00:00:01 |
|   5 |    TABLE ACCESS FULL     | EMP     |    14 |   182 |     3 (0)| 00:00:01 |
----------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
   4 - access("A"."DEPTNO"="B"."DEPTNO")
       filter("A"."DEPTNO"="B"."DEPTNO")

首先要關注是對dept表所使用的索引掃描,這個例子中,因爲索引將按順序後順序返回數據,優化器選擇使用索引來讀取表數據,這就意味着可以避免一次單獨的排序運算。對於emp表必須進行全表掃描然後單獨排序,因爲的盤太濃這一列上沒有索引可以用。在兩個數據行集都準備好並排序後,他們講被合併到一起。

排序--合併聯結將會訪問所需的數據塊然後再內存中(或者如果沒有足夠內存的話使用臨時磁盤空間)進行排序和合並。因此,當你對排序-合併和嵌套循環聯結進行邏輯讀取比較的時候,尤其是對於一個更大的數據行源的查詢。你可能發現嵌套循環聯結所需訪問的塊更多。此時,排序-合併也並非更好的選擇,你必須把所有需要進行排序已經合併等步驟的工作都考慮進去,並意識到這些工作最終可能比做較多的塊訪問花費的時候跟多。

排序-合併聯結一般最時候於數據篩選條件有限並返回有限數據行的查詢,如果沒有可用的更直接訪問數據行的索引時排序-合併聯結通常是最好的選擇。總的來說,在條件爲非等式的時候排序-合併聯結通常是最好的選擇,例如 where table.column1 between table2。column1 and table2.columns這樣聯結條件就比較適合排序-合併聯結。

1.3 散列聯結

散列聯結,與排序-合併聯結類似,首先應用where子句中的篩選標準來獨立地讀取要進行聯結的兩個表。基於表和索引的統計信息,被確定爲返回最少行數的表完全散列化到內存中。這個散列表包含原表的所有數據行並被基於講聯結鍵轉化爲散列值的隨機函數載人到散列桶中。只有有足夠的內存空間,這個散列表將一直放在內存中。然而,如果沒有足夠的內存,散列表被寫入到臨時磁盤空間。

下一步就是讀取另一張較大的表並對聯結鍵列應用散列函數。然後利用得到的散列值對較小在內存中的散列表進行探測以尋找匹配的第一個表的行數據所在散列桶,每個散列桶都有一個放在其中的數據行列表(通過一個位圖來表示)。這個列表被用來與探測進行匹配,如果匹配成功,則返回這一行數據,否則丟棄。較大的表只讀取一次,並檢查其中的每一行來尋找匹配。這個與嵌套循環聯結的不同之處在於此處內存表被多次讀取。因此事實上在這個例子中,較大的表是驅動表,因此僅讀取一次。而較小的散列表則被探測很多次,與嵌套迴路聯結計劃不同,在執行計劃的輸出中較小的散列表放在前面而較大的探測表放在後面。

在散列聯結的執行計劃中,較小的散列表列在前面而探測表列在後面。記住這一點就是決定哪個表是最小的不僅取決於數據行還取決於這些行的大小,因爲整個行都必須要存放在散列表中。

當數據行源較大並且結果集也較大的情況下將更傾向於考慮散列聯結。或者,如果要散結的其中一張表確定總是返回同一數據行源,也可能會選用散列聯結因爲這樣僅訪問一次這張表,如果在這種情況下選用嵌套循環聯結,這個數據行源就會被一遍又一遍地訪問,需要比單獨訪問一次多做很多工作。最後,如果較小的表可以放到內存中,散列聯結也是適用的。

散列聯結中訪問數據塊的方式類似於排序-合併聯結中訪問方式。建立散列表所需的數據塊將被讀取,然後剩下的工作將會針對存放的內存中(如果沒有足夠內存的話將會是臨時磁盤空間)的散列數據進行。因此,當你對散列聯結和排序-合併聯結的邏輯讀取進行比較的時候,訪問的數據塊幾乎是相等的。但相比於嵌套循環聯結來說邏輯讀取比較少。因爲數據塊僅被讀取了一次,然後或者存放在內存中(對於散列表)再進行訪問或只讀取一次(探測表)

散列聯結只有在相等聯結的情況下才能進行,如前面所提到的,排序-合併聯結可以用來處理特定的非等試的條件。散列聯結只有在相等聯結時才能選用的原因在於匹配是針對散列值來進行的,而在一個範圍內來考慮散列值是沒有意義的。

散列值並不必然等於散列鍵值:

SQL> select distinct deptno,ora_hash(deptno,1000) hv from emp order by deptno;

    DEPTNO   HV
---------- ----------
10  547
20  486
30  613

SQL> select deptno from (select distinct deptno,ora_hash(deptno,1000) hv from emp order by deptno) where hv between 100 and 500;
    DEPTNO
----------
20


SQL> select distinct deptno,ora_hash(deptno,1000,50) hv from emp order by deptno;

    DEPTNO   HV
---------- ----------
10  839
20  850
30  290

SQL> select deptno from (select distinct deptno,ora_hash(deptno,1000,50) hv from emp order by deptno) where hv between 100 and 500;
    DEPTNO
----------
30

我利用ora_hash函數來說明散列值是如何生成的。ora_hash函數有3個參數:一個任何基本類型的輸入,最大散列桶值(最小值爲0)以及一個種子值(默認值也是0)。

上述例子爲了理解散列聯結不適用非等式聯結。

1.4  笛卡兒聯結

笛卡兒聯結髮生在當一張表中的所有行與另一張表中所有行聯結的時候。因此,這種聯結所得到的結果集的總行數等於一張表(A)中的數據行數乘以另一張表(B)中的數據行數,也就是A*B=結果集中總數的數據行數。通常當聯結條件被忽略或忽視以致沒有指定的聯結列,所能做的唯一可能的運算就是將一張表的所有內容與另一張表的所有內容聯結起來的時候會進行笛卡兒聯結。

SQL> select empno,ename,dname,loc from emp,dept;
Execution Plan
----------------------------------------------------------
Plan hash value: 2034389985
-----------------------------------------------------------------------------
| Id  | Operation     | Name | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------
|   0 | SELECT STATEMENT     |    | 56 |  1568 | 10   (0)| 00:00:01 |
|   1 |  MERGE JOIN CARTESIAN|    | 56 |  1568 | 10   (0)| 00:00:01 |
|   2 |   TABLE ACCESS FULL  | DEPT |  4 | 72 |  3   (0)| 00:00:01 |
|   3 |   BUFFER SORT     |    | 14 | 140 |  7   (0)| 00:00:01 |
|   4 |    TABLE ACCESS FULL | EMP  | 14 | 140 |  2   (0)| 00:00:01 |
-----------------------------------------------------------------------------

關於笛卡兒聯結執行計劃需要注意的一點就是BUFFER SORT運算的出現,這並不是一次真正的排序。而是由於oracle將每一行與每一行進行聯結,使用緩衝區排序機制將第二張的數據塊從緩衝區緩存複製出並放入到私有內存中,避免緩衝區緩存中的同一個數據塊被一次又一次的訪問。這些重複訪問需要大量的邏輯讀取,並且也會增加更多對緩衝區緩存中的這些數據塊進行爭奪的機率。因此,將這些數據塊緩存到一個私有內存區域可能是一種更有效的實現重複聯結的方式。

1.5 外聯結

外聯結返回一張表的所有行以及另一張聯結表中滿足聯結條件的行數據。oracle使用+字符來表明進行外聯結,+號放在一對圓括號中,位於只有匹配纔會返回數據行的表聯結條件旁。這意味着有可能選用更加優化的聯結執行順序。因此,使用外聯結的時候要格外小心,因爲它的選用有可能會影響到整個執行計劃的性能。

SELECT c.cust_last_name, nvl(SUM(o.order_total), 0) tot_orders
  FROM customers c, orders o
 WHERE c.customer_id = o.customer_id
 GROUP BY c.cust_last_name
HAVING nvl(SUM(o.order_total), 0) BETWEEN 0 AND 5000
  6   ORDER BY c.cust_last_name;
CUST_LAST_NAME     TOT_ORDERS
-------------------- ----------
Alexander    309
Chandar    510
George    220
Hershey     48
Higgins    416
Kazan   1233
Sen   4797
Stern  969.2
Weaver    600
9 rows selected.

SELECT COUNT(*) ct
  FROM (SELECT c.cust_last_name, nvl(SUM(o.order_total), 0) tot_orders
          FROM customers c, orders o
         WHERE c.customer_id = o.customer_id
         GROUP BY c.cust_last_name
        HAVING nvl(SUM(o.order_total), 0) BETWEEN 0 AND 5000
  7           ORDER BY c.cust_last_name);

CT
----------
9

SELECT COUNT(*) ct
  FROM (SELECT c.cust_last_name, nvl(SUM(o.order_total), 0) tot_orders
          FROM customers c, orders o
         WHERE c.customer_id = o.customer_id(+)
         GROUP BY c.cust_last_name
        HAVING nvl(SUM(o.order_total), 0) BETWEEN 0 AND 5000
  7           ORDER BY c.cust_last_name);

CT
----------
       140


Execution Plan
----------------------------------------------------------
Plan hash value: 2278331185

-----------------------------------------------------------------------------------------------------
| Id  | Operation | Name    | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT |    |  1 |    |  5  (20)| 00:00:01 |
|   1 |  SORT AGGREGATE |    |  1 |    | |    |
|   2 |   VIEW |    |  1 |    |  5  (20)| 00:00:01 |
|*  3 |    FILTER |    |    |    | |    |
|   4 |     HASH GROUP BY |    |  1 | 22 |  5  (20)| 00:00:01 |
|*  5 |      HASH JOIN OUTER |    | 319 |  7018 |  4   (0)| 00:00:01 |
|   6 |       VIEW | index$_join$_002 | 319 |  3828 |  2   (0)| 00:00:01 |
|*  7 |        HASH JOIN |    |    |    | |    |
|   8 | INDEX FAST FULL SCAN | CUSTOMERS_PK     | 319 |  3828 |  1   (0)| 00:00:01 |
|   9 | INDEX FAST FULL SCAN | CUST_LNAME_IX    | 319 |  3828 |  1   (0)| 00:00:01 |
|  10 |       TABLE ACCESS BY INDEX ROWID| ORDERS    | 105 |  1050 |  2   (0)| 00:00:01 |
|* 11 |        INDEX RANGE SCAN | ORD_CUSTOMER_IX  | 105 |    |  1   (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------------

從上面的例子可以看到最初沒有使用外聯結的結果不是十分正確,因爲還沒有下單的顧客在訂單order表中是沒有記錄的,他們將不會被包含在最終的查詢結果集中。將查詢修改爲外聯結將會把這些顧客包含進來,同時還要注意執行計劃的第5步指定了HASH JOIN OUTER。外聯結可以與任何聯結方法(嵌套循環,散列,排序--合併)一起使用。並通過紮起常規運算名稱後面加上outer命令來表示。

通過ANSI聯結語法的外聯結:

SELECT COUNT(*) ct
  FROM (SELECT c.cust_last_name, nvl(SUM(o.order_total), 0) tot_orders
          FROM customers c
          LEFT OUTER JOIN orders o
            ON (c.customer_id = o.customer_id)
         GROUP BY c.cust_last_name
        HAVING nvl(SUM(o.order_total), 0) BETWEEN 0 AND 5000
         ORDER BY c.cust_last_name);

在ANSI語法中,你只需要使用關鍵字left outer join。這表明了左側的表(也就是說所列出的第一表)是你需要的即使沒有滿足聯結條件的數據行也要將所有的行包含在結果集中的表。如果你想使用即使customers表中沒有對應匹配,也要返回orders表中的所有數據行,你可以使用right outer join關鍵字

當使用oracle的(+)運算時,會有一些在使用ANSI語法時沒有的侷限性,如果你嘗試將通一個表與另外多張表進行外聯結oracle將會拋出一個錯誤,ORA-014117:一張表最多隻能與另外一張表進行外聯結。而使用ANSI語法對錶的數目沒有限制。即使是一張表可以外聯結。

oracle外聯結語法的另一侷限性在於它不支持全外聯結。

SELECT e1.empno, e1.deptno, e1.job, e2.ename, e2.deptno, e2.job
  FROM e1
  FULL OUTER JOIN e2
  4      ON (e1.empno = e2.empno);

     EMPNO     DEPTNO JOB ENAME       DEPTNO JOB
---------- ---------- --------- ---------- ---------- ---------
      7369   20 CLERK SMITH   20 CLERK
ALLEN   30 SALESMAN
WARD   30 SALESMAN
      7566   20 MANAGER JONES   20 MANAGER
MARTIN   30 SALESMAN
BLAKE   30 MANAGER
      7788   20 ANALYST SCOTT   20 ANALYST
TURNER   30 SALESMAN
      7876   20 CLERK ADAMS   20 CLERK
JAMES   30 CLERK
      7902   20 ANALYST FORD   20 ANALYST

     EMPNO     DEPTNO JOB ENAME       DEPTNO JOB
---------- ---------- --------- ---------- ---------- ---------
      7839   10 PRESIDENT
      7782   10 MANAGER
      7934   10 CLERK

14 rows selected.

----------------------------------------------------------------------------------
| Id  | Operation      | Name | Rows  | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT      | |    11 |   638 |     6   (0)| 00:00:01 |
|   1 |  VIEW      | VW_FOJ_0 |    11 |   638 |     6   (0)| 00:00:01 |
|*  2 |   HASH JOIN FULL OUTER| |    11 |   396 |     6   (0)| 00:00:01 |
|   3 |    TABLE ACCESS FULL  | E1 |     8 |   120 |     3   (0)| 00:00:01 |
|   4 |    TABLE ACCESS FULL  | E2 |    11 |   231 |     3   (0)| 00:00:01 |
----------------------------------------------------------------------------------

使用oracle的等價寫法:

SELECT e1.empno, e1.deptno, e1.job, e2.ename, e2.deptno, e2.job
  FROM e1, e2
 WHERE e1.empno(+) = e2.empno
UNION ALL
SELECT e1.empno, e1.deptno, e1.job, e2.ename, e2.deptno, e2.job
  FROM e1, e2
  7   WHERE e1.empno = e2.empno(+)
  8  ;

Execution Plan
----------------------------------------------------------
Plan hash value: 3338363451
----------------------------------------------------------------------------
| Id  | Operation    | Name | Rows  | Bytes | Cost (%CPU)| Time   |
----------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |   | 19 |   684 | 12   (0)| 00:00:01 |
|   1 |  UNION-ALL    |   |   |   | |   |
|*  2 |   HASH JOIN OUTER   |   | 11 |   396 | 6   (0)| 00:00:01 |
|   3 |    TABLE ACCESS FULL| E2   | 11 |   231 | 3   (0)| 00:00:01 |
|   4 |    TABLE ACCESS FULL| E1   | 8 |   120 | 3   (0)| 00:00:01 |
|*  5 |   HASH JOIN OUTER   |   | 8 |   288 | 6   (0)| 00:00:01 |
|   6 |    TABLE ACCESS FULL| E1   | 8 |   120 | 3   (0)| 00:00:01 |
|   7 |    TABLE ACCESS FULL| E2   | 11 |   231 | 3   (0)| 00:00:01 |
----------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("E1"."EMPNO"(+)="E2"."EMPNO")
   5 - access("E1"."EMPNO"="E2"."EMPNO"(+))

全外聯結就其執行所需要的資源來說成本會很高。使用的時候需要小心地理解編寫這樣的查詢所帶來的影響並注意其對性能的影響。

小結:在確定SQL語句的執行計劃的時候,優化器 必須做出幾個關鍵的選擇。首先,要確定查詢胡總所用到的每個表最適合的訪問方法。基本上有兩種選擇:索引掃描和全表掃描。每種訪問方法用來訪問包含SQL語句所需數據的實現方式是不同的。一旦優化器選定了訪問方法,就必須選定聯結方法。表將會被逐對聯結,前一次聯結的結果的數據行被用來與下一個表聯結。直到所有表都被聯結並獲得最終的結果集。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章