查詢訪問同一表的兩個以上索引(三)

yangtingkun發表於2010-05-05

經常看到有人提出這樣的疑問,我在同一張表上建立了多個索引,為什麼Oracle每次都選擇一個,而不能同時利用多個索引呢。一般來說,常見的訪問同一張表的兩個以上索引,存在三種情況,AND-EQUALINDEX HASH JOINBITMAP INDEX AND/OR

此外,還有一個設計上的疑問,如果有ABC三個欄位,都可能作為查詢條件,是建立多個複合索引好,還是建立三個單列的索引。這個問題之所以不好回答是因為和業務或者說和查詢的模式有很大的關係,不過如果理解了Oracle什麼時候會選擇一個以上的索引來訪問表,就會對於理解如何設計合理的索引有很大的幫助。

簡單介紹一下INDEX HASH JOIN執行計劃。

查詢訪問同一表的兩個以上索引(一):http://yangtingkun.itpub.net/post/468/499769

查詢訪問同一表的兩個以上索引(二):http://yangtingkun.itpub.net/post/468/499798

 

 

首先建立一個測試表:

SQL> DROP TABLE T_DOUBLE_IND PURGE;

Table dropped.

SQL> CREATE TABLE T_DOUBLE_IND
  2  (ID NUMBER,
  3  NAME VARCHAR2(30),
  4  TYPE VARCHAR2(30),
  5  CONTENTS VARCHAR2(4000));

Table created.

SQL> INSERT INTO T_DOUBLE_IND
  2  SELECT ROWNUM,
  3  OBJECT_NAME,
  4  OBJECT_TYPE,
  5  LPAD('A', 1000, 'A')
  6  FROM DBA_OBJECTS;

76058 rows created.

SQL> CREATE INDEX IND_DOUBLE_NAME
  2  ON T_DOUBLE_IND (NAME);

Index created.

SQL> CREATE INDEX IND_DOUBLE_TYPE
  2  ON T_DOUBLE_IND (TYPE);

Index created.

現在建立了一個測試表和兩個索引,下面看看如何利用索引雜湊連線:

SQL> EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'T_DOUBLE_IND')

PL/SQL procedure successfully completed.

SQL> SET AUTOT ON EXP
SQL> SELECT NAME, TYPE
  2  FROM T_DOUBLE_IND
  3  WHERE NAME = 'T_DOUBLE_IND'
  4  AND TYPE = 'TABLE';

NAME                           TYPE
------------------------------ ------------------------------
T_DOUBLE_IND                   TABLE


Execution Plan
----------------------------------------------------------
Plan hash value: 3887138334

--------------------------------------------------------------------------------------------
| Id| Operation                   | Name            | Rows | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT            |                 |    1 |    32 |     3   (0)| 00:00:01 |
|*1 |  TABLE ACCESS BY INDEX ROWID| T_DOUBLE_IND    |    1 |    32 |     3   (0)| 00:00:01 |
|*2 |   INDEX RANGE SCAN          | IND_DOUBLE_NAME |    2 |       |     1   (0)| 00:00:01 |
--------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - filter("TYPE"='TABLE')
   2 - access("NAME"='T_DOUBLE_IND')

SQL> SELECT /*+ INDEX_JOIN(A IND_DOUBLE_NAME IND_DOUBLE_TYPE) */ NAME, TYPE
  2  FROM T_DOUBLE_IND A
  3  WHERE NAME = 'T_DOUBLE_IND'
  4  AND TYPE = 'TABLE';

NAME                           TYPE
------------------------------ ------------------------------
T_DOUBLE_IND                   TABLE


Execution Plan
----------------------------------------------------------
Plan hash value: 1096369674

---------------------------------------------------------------------------------------
| Id  | Operation          | Name             | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |                  |     1 |    32 |     6  (17)| 00:00:01 |
|*  1 |  VIEW              | index$_join$_001 |     1 |    32 |     6  (17)| 00:00:01 |
|*  2 |   HASH JOIN        |                  |       |       |            |          |
|*  3 |    INDEX RANGE SCAN| IND_DOUBLE_NAME  |     1 |    32 |     1   (0)| 00:00:01 |
|*  4 |    INDEX RANGE SCAN| IND_DOUBLE_TYPE  |     1 |    32 |     4   (0)| 00:00:01 |
---------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - filter("NAME"='T_DOUBLE_IND' AND "TYPE"='TABLE')
   2 - access(ROWID=ROWID)
   3 - access("NAME"='T_DOUBLE_IND')
   4 - access("TYPE"='TABLE')

利用INDEX_JOIN提示,可以使得Oracle選擇INDEX HASH JOIN連線方式,從而避免對錶進行訪問。Oracle選擇兩個索引進行範圍掃描,然後執行HASH JOIN,而HASH JOIN的連線列是ROWID,最後透過一個內部檢視執行查詢條件的過濾。

不過這種執行計劃的前提是,查詢所選擇的列必須能夠透過索引完全的提供,如果包含了索引中不存在的列,Oracle是不會再次選擇掃描表的。

SQL> SELECT /*+ INDEX_JOIN(A IND_DOUBLE_NAME IND_DOUBLE_TYPE) */ ID, NAME, TYPE
  2  FROM T_DOUBLE_IND A
  3  WHERE NAME = 'T_DOUBLE_IND'
  4  AND TYPE = 'TABLE';

        ID NAME                           TYPE
---------- ------------------------------ ------------------------------
     70924 T_DOUBLE_IND                   TABLE


Execution Plan
----------------------------------------------------------
Plan hash value: 3887138334

--------------------------------------------------------------------------------------------
| Id| Operation                   | Name            | Rows | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT            |                 |    1 |    37 |     3   (0)| 00:00:01 |
|*1 |  TABLE ACCESS BY INDEX ROWID| T_DOUBLE_IND    |    1 |    37 |     3   (0)| 00:00:01 |
|*2 |   INDEX RANGE SCAN          | IND_DOUBLE_NAME |    2 |       |     1   (0)| 00:00:01 |
--------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - filter("TYPE"='TABLE')
   2 - access("NAME"='T_DOUBLE_IND')

不僅如此,就是選擇了索引中包含的ROWID資訊,Oracle仍然會不會選擇INDEX HASH JOIN

SQL> SELECT /*+ INDEX_JOIN(A IND_DOUBLE_NAME IND_DOUBLE_TYPE) */ ROWID, NAME, TYPE
  2  FROM T_DOUBLE_IND A
  3  WHERE NAME = 'T_DOUBLE_IND'
  4  AND TYPE = 'TABLE';

ROWID              NAME                           TYPE
------------------ ------------------------------ ------------------------------
AAAjc5AAVAAAqQUAAD T_DOUBLE_IND                   TABLE


Execution Plan
----------------------------------------------------------
Plan hash value: 3887138334

--------------------------------------------------------------------------------------------
| Id| Operation                   | Name            | Rows | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT            |                 |    1 |    44 |     3   (0)| 00:00:01 |
|*1 |  TABLE ACCESS BY INDEX ROWID| T_DOUBLE_IND    |    1 |    44 |     3   (0)| 00:00:01 |
|*2 |   INDEX RANGE SCAN          | IND_DOUBLE_NAME |    2 |       |     1   (0)| 00:00:01 |
--------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - filter("TYPE"='TABLE')
   2 - access("NAME"='T_DOUBLE_IND')

其實這是沒有道理的,因為索引中本身就包括ROWID資訊,而且兩個索引做HASH JOIN的時候,連線列就是ROWID,因此這個查詢本來應該可以透過INDEX HASH JOIN來實現的,可能CBO最佳化器在處理這個演算法的時候忽略了這個問題。

當然,查詢COUNT(*)的操作也是可以利用INDEX HASH JOIN的:

SQL> SELECT /*+ INDEX_JOIN(A IND_DOUBLE_NAME IND_DOUBLE_TYPE) */ COUNT(*)
  2  FROM T_DOUBLE_IND A
  3  WHERE NAME = 'T_DOUBLE_IND'
  4  AND TYPE = 'TABLE';

  COUNT(*)
----------
         1


Execution Plan
----------------------------------------------------------
Plan hash value: 2001662014

----------------------------------------------------------------------------------------
| Id  | Operation           | Name             | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |                  |     1 |    32 |     6  (17)| 00:00:01 |
|   1 |  SORT AGGREGATE     |                  |     1 |    32 |            |          |
|*  2 |   VIEW              | index$_join$_001 |     1 |    32 |     6  (17)| 00:00:01 |
|*  3 |    HASH JOIN        |                  |       |       |            |          |
|*  4 |     INDEX RANGE SCAN| IND_DOUBLE_NAME  |     1 |    32 |     1   (0)| 00:00:01 |
|*  5 |     INDEX RANGE SCAN| IND_DOUBLE_TYPE  |     1 |    32 |     4   (0)| 00:00:01 |
----------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter("NAME"='T_DOUBLE_IND' AND "TYPE"='TABLE')
   3 - access(ROWID=ROWID)
   4 - access("NAME"='T_DOUBLE_IND')
   5 - access("TYPE"='TABLE')

Oracle會自動根據統計資訊權衡是否選擇INDEX HASH JOIN,而不需要透過HINT來強制執行:

SQL> SET AUTOT OFF
SQL> UPDATE T_DOUBLE_IND
  2  SET NAME = 'T_DOUBLE_IND'
  3  WHERE ID <= 2000;

2000 rows updated.

SQL> EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'T_DOUBLE_IND')

PL/SQL procedure successfully completed.

SQL> SET AUTOT TRACE EXP
SQL> SELECT NAME, TYPE
  2  FROM T_DOUBLE_IND A
  3  WHERE NAME = 'T_DOUBLE_IND'
  4  AND TYPE = 'TABLE';

Execution Plan
----------------------------------------------------------
Plan hash value: 1096369674

---------------------------------------------------------------------------------------
| Id  | Operation          | Name             | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT   |                  |   259 |  8288 |    19   (6)| 00:00:01 |
|*  1 |  VIEW              | index$_join$_001 |   259 |  8288 |    19   (6)| 00:00:01 |
|*  2 |   HASH JOIN        |                  |       |       |            |          |
|*  3 |    INDEX RANGE SCAN| IND_DOUBLE_NAME  |   259 |  8288 |     5   (0)| 00:00:01 |
|*  4 |    INDEX RANGE SCAN| IND_DOUBLE_TYPE  |   259 |  8288 |    13   (0)| 00:00:01 |
---------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - filter("NAME"='T_DOUBLE_IND' AND "TYPE"='TABLE')
   2 - access(ROWID=ROWID)
   3 - access("NAME"='T_DOUBLE_IND')
   4 - access("TYPE"='TABLE')

但是由於INDEX HASH JOIN的限制,使得這個查詢只能滿足查詢索引列或COUNT(*)的情況,因此使得這個執行計劃的使用機會大大降低。

而如果建立複合索引,講會使得查詢代價大為降低,而且可以滿足繼續掃描表的查詢要求:

SQL> CREATE INDEX IND_DOU_NAM_TYP
  2  ON T_DOUBLE_IND (NAME, TYPE);

Index created.

SQL> EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'T_DOUBLE_IND')

PL/SQL procedure successfully completed.

SQL> SELECT NAME, TYPE
  2  FROM T_DOUBLE_IND A
  3  WHERE NAME = 'T_DOUBLE_IND'
  4  AND TYPE = 'TABLE';

Execution Plan
----------------------------------------------------------
Plan hash value: 3922347956

------------------------------------------------------------------------------------
| Id  | Operation        | Name            | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT |                 |   240 |  7680 |     1   (0)| 00:00:01 |
|*  1 |  INDEX RANGE SCAN| IND_DOU_NAM_TYP |   240 |  7680 |     1   (0)| 00:00:01 |
------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - access("NAME"='T_DOUBLE_IND' AND "TYPE"='TABLE')

SQL> SELECT ID, NAME, TYPE
  2  FROM T_DOUBLE_IND A
  3  WHERE NAME = 'T_DOUBLE_IND'
  4  AND TYPE = 'TABLE';

Execution Plan
----------------------------------------------------------
Plan hash value: 1808990274

--------------------------------------------------------------------------------------------
| Id  | Operation                   | Name            | Rows | Bytes | Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT            |                 |  240 |  8880 |   149   (0)| 00:00:03 |
| 1 |  TABLE ACCESS BY INDEX ROWID| T_DOUBLE_IND    |  240 |  8880 |   149   (0)| 00:00:03 |
|*2 |   INDEX RANGE SCAN          | IND_DOU_NAM_TYP |  240 |       |     1   (0)| 00:00:01 |
--------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - access("NAME"='T_DOUBLE_IND' AND "TYPE"='TABLE')

簡單總結一下,根據這三篇文章介紹的同時訪問兩個以上索引的三種情況而言,AND-EQUAL執行計劃在Oracle已經不在推薦,連AND-EQUAL這個HINT在文件中也不再介紹,只是為了後向相容性而繼續保留。原因已經介紹過,Oracle無法透過統計資訊來判斷這種執行路徑的代價。而INDEX HASH JOIN則由於限制條件的問題很難廣泛使用。唯一對於多個單列索引而言的優勢在於BITMAP AND/OR操作,而一般這種執行計劃在資料倉儲型別的查詢中更為常見。因此,對於要求快速得到查詢結果的OLTP系統中,複合索引應該是更為合理的選擇,不要希望透過建立多個單列索引,來完全代替複合索引的建立。

最後,什麼索引最適合你的系統仍然要具體情況具體分析,實踐才是檢驗索引的唯一真理。

 

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

相關文章