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

yangtingkun發表於2010-05-03

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

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

簡單介紹一下AND-EQUAL執行計劃。

 

 

首先建立一個測試表:

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;

75856 rows created.

SQL> COMMIT;

Commit complete.

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.

現在建立了一個測試表和兩個索引,下面首先來看看AND-EQUAL執行方式:

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

        ID NAME                           TYPE
---------- ------------------------------ ------------------------------
     75688 T_DOUBLE_IND                   TABLE


Execution Plan
----------------------------------------------------------
Plan hash value: 474554719

-------------------------------------------------------
| Id  | Operation                   | Name            |
-------------------------------------------------------
|   0 | SELECT STATEMENT            |                 |
|   1 |  TABLE ACCESS BY INDEX ROWID| T_DOUBLE_IND    |
|   2 |   AND-EQUAL                 |                 |
|*  3 |    INDEX RANGE SCAN         | IND_DOUBLE_NAME |
|*  4 |    INDEX RANGE SCAN         | IND_DOUBLE_TYPE |
-------------------------------------------------------

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

   3 - access("NAME"='T_DOUBLE_IND')
   4 - access("TYPE"='TABLE')

Note
-----
   - rule based optimizer used (consider using cbo)

由於指定了兩個列,且兩個列上都包含索引,Oracle選擇了掃描兩個索引,並使用了AND-EQUAL執行計劃。這種掃描方式是分別透過兩個索引獲取索引鍵值對應的ROWID,然後合併兩個掃描中相等的ROWID,並透過這個ROWID來掃描表。

不過觀察執行計劃可以發現,當前的最佳化模式是RBO,如果收集一下索引,再次執行同樣的查詢:

SQL> SHOW PARAMETER OPTIMIZER_MODE

NAME                                 TYPE        VALUE
------------------------------------ ----------- ------------------------------
optimizer_mode                       string      CHOOSE
SQL> EXEC DBMS_STATS.GATHER_TABLE_STATS(USER, 'T_DOUBLE_IND')

PL/SQL procedure successfully completed.

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

        ID NAME                           TYPE
---------- ------------------------------ ------------------------------
     75688 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')

可以看到,收集統計資訊後,最佳化模式變為CBO,但是Oracle只選擇了一個索引進行掃描。對於當前的情況,NAME列的選擇度非常高,因此這種方式的代價最低。

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

        ID NAME                           TYPE
---------- ------------------------------ ------------------------------
     75689 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')

即使透過HINT指定兩個索引,CBO最佳化器也只是會選擇其中一個索引來進行掃描,而自動忽略另一個選擇度低的索引。

只有使用AND_EQUAL提示,才能在CBO的情況下使用AND-EQUAL執行計劃:

SQL> SELECT /*+ AND_EQUAL(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';

Execution Plan
----------------------------------------------------------
Plan hash value: 474554719

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

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

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

CBO下,Oracle不會自動選擇AND-EQUAL執行計劃,這是因為Oracle可以根據直方圖來判斷一個列上各個值的選擇度,但是從統計資訊無法獲得兩個不同的列合AND-EQUAL之後的選擇度,這個操作後,如果得到的ROWID記錄很少,那麼這個查詢的效率就會很高,如果AND-EQUAL後得到大量的ROWID,那麼查詢的效率就會很差,這時應該選擇全表掃描或其他的執行計劃。由於CBO都是根據統計資訊分析得到的結果,而AND-EQUAL的結果對於CBO是未知的,因此CBO不會選擇AND-EQAUL這種掃描方式。

如果要Oracle獲得這種統計資訊,最簡單的方法就是建立一個複合索引,Oracle在分析索引列的時候自動會分析兩個列的組合情況,從而在查詢的時候可以準確的判斷是否應該使用這個複合索引。

SQL> SET AUTOT OFF
SQL> CREATE INDEX IND_DOU_NAM_TYP
  2  ON T_DOUBLE_IND
  3  (NAME, TYPE);

Index created.

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

PL/SQL procedure successfully completed.

SQL> SET AUTOT TRACE EXP
SQL> SELECT ID, NAME, TYPE
  2  FROM T_DOUBLE_IND
  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            |                 |    2 |    74 |     3   (0)| 00:00:01 |
| 1 |  TABLE ACCESS BY INDEX ROWID| T_DOUBLE_IND    |    2 |    74 |     3   (0)| 00:00:01 |
|*2 |   INDEX RANGE SCAN          | IND_DOU_NAM_TYP |    2 |       |     1   (0)| 00:00:01 |
--------------------------------------------------------------------------------------------

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

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

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

20000 rows updated.

SQL> COMMIT;

Commit complete.

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

PL/SQL procedure successfully completed.

SQL> SET AUTOT TRACE EXP
SQL> SELECT ID, NAME, TYPE
  2  FROM T_DOUBLE_IND
  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            |                 | 2277 | 77418 |  1225   (1)| 00:00:18 |
| 1 |  TABLE ACCESS BY INDEX ROWID| T_DOUBLE_IND    | 2277 | 77418 |  1225   (1)| 00:00:18 |
|*2 |   INDEX RANGE SCAN          | IND_DOU_NAM_TYP | 2277 |       |    11   (0)| 00:00:01 |
--------------------------------------------------------------------------------------------

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

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

SQL> SET AUTOT OFF
SQL> UPDATE T_DOUBLE_IND
  2  SET TYPE = 'TABLE'
  3  WHERE ID <= 20000;

20000 rows updated.

SQL> COMMIT;

Commit complete.

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

PL/SQL procedure successfully completed.

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

Execution Plan
----------------------------------------------------------
Plan hash value: 4134729579

----------------------------------------------------------------------------------
| Id  | Operation         | Name         | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |              |  7778 |   258K|  2010   (1)| 00:00:29 |
|*  1 |  TABLE ACCESS FULL| T_DOUBLE_IND |  7778 |   258K|  2010   (1)| 00:00:29 |
----------------------------------------------------------------------------------

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

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

在建立複合索引後,對於剛才的查詢,Oracle會自動選擇複合索引。即使將表中的20000條記錄全部更新為NAME=’T_DOUBLE_BIN’,由於TYPE列的選項條件,在加上索引相對於表來說要窄得多,Oracle仍然選擇了複合索引掃描。

如果將TYPE列也更新20000條記錄,使得NAME=’T_DOUBLE_IND’TYPE=’TABLE’的選擇性很差,這時Oracle就會自動選擇全表掃描來代替索引掃描。

 

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

相關文章