這節將介紹各種索引掃描方式,在了解了各種索引掃描方式的特點後,你就可以判斷你的執行計劃中使用的掃描方式是否正確,並可以針對獲取的信息作出改進。
在下面的場景中使用相等條件時,數據庫使用索引唯一掃描。
1)查詢條件中包含唯一索引中的所有列時;
2)查詢條件使用主鍵約束列時。
下面是一個實際的例子,在表historyalarm中創建如下唯一索引:
create unique index idx_historyalarm$queryid on historyalarm(queryid) tablespace uep4x_fm_index
然後在表上執行查詢:
select * from historyalarm where queryid = 3
該查詢符合上面的第一種情況,會使用索引唯一掃描,該查詢的執行計劃如下:
DESCRIPTION OBJECT NAME ----------------------------------------------------------------------- SELECT STATEMENT, GOAL = ALL_ROWS TABLE ACCESS BY GLOBAL INDEX ROWID HISTORYALARM INDEX UNIQUE SCAN IDX_HISTORYALARM$QUERYID
這裡Oracle首先通過唯一索引掃描找到索引節點,然後使用索引節點中包含的rowid來訪問表中的數據。
當查詢條件可能會返回一定范圍的數據時就會選用索引范圍掃描,索引可以是唯一索引或者不唯一索引,但如果查詢條件包含的數據范圍太大,也有可能導致全表掃描。查詢條件中使用<、>、LIKE、BETWEEN、=等都可能使用索引范圍掃描,需要注意單個=條件在唯一索引或者主鍵上將導致索引唯一掃描。
下面是一個索引范圍掃描的例子,在上面的histroyalarm中執行一個范圍查詢:
select * from caffm4x.historyalarm where queryid < 10
這裡查詢的數據是一個范圍,且使用了queryid列,在queryid列上有唯一索引,但任然會導致索引范圍掃描:
DESCRIPTION OBJECT NAME ----------------------------------------------------------------------- SELECT STATEMENT, GOAL = ALL_ROWS TABLE ACCESS BY GLOBAL INDEX ROWID HISTORYALARM INDEX RANGE SCAN IDX_HISTORYALARM$QUERYID
索引范圍掃描從索引的根節點出發,找到第一個匹配的條目所在的葉子數據塊開始遍歷索引結構,首先從索引條目中取出rowid然後取出對應的表數據塊(通過rowid訪問數據表),接下來葉子索引塊會被再次訪問並讀取下一個索引條目並獲取rowid,這樣反復直到整個葉子索引塊被的索引條目全部被讀出。因此排除索引根節點和中間節點,每行數據讀取需要讀取兩個數據塊,我們可以通過blevel來得到索引高度,通過索引高度和獲取的數據行數就能得到需要讀取的數據塊數,例如:如果blevel為3,讀取5行數據,則總的需要訪問的數據塊次數將是(5*2)
+ 3 = 13(注意只有根節點時blevel為0)。
如果在讀取了整個葉子索引塊之後,還需要訪問下一個葉子索引塊,在當前的葉子索引塊中有指向下一個葉子索引塊的指針(也含有指向上一個葉子索引塊的指針)。
使用索引范圍掃描的另一個優勢就在於排序,由於索引的節點是有序的,因此如果查詢的結果需要按照索引列排序(升序或者降序),那麼使用索引范圍掃描則可以很好的避免排序操作,例如:
select * from historyalarm where queryid > 10
由於queryid大於10的數據量占總數據量的99%,因此Oracle的優化器選擇了全表掃描:
DESCRIPTION OBJECT NAME ----------------------------------------------------------------------- SELECT STATEMENT, GOAL = ALL_ROWS PARTITION RANGE ALL TABLE ACCESS FULL HISTORYALARM
如果我們在查詢時對數據指定排序,如下:
select * from historyalarm where queryid > 10 order by queryid
執行計劃如下:
DESCRIPTION OBJECT NAME ----------------------------------------------------------------------- SELECT STATEMENT, GOAL = ALL_ROWS TABLE ACCESS BY GLOBAL INDEX ROWID HISTORYALARM INDEX RANGE SCAN IDX_HISTORYALARM$QUERYID
優化器改為使用了索引范圍掃描。由於當數據量很大時,排序的代價是很大的(可能導致物理排序),這時使用索引范圍掃描將是一個很好的選擇,特別是當你排序後選擇部分數據的情況下(rownum < n)。
索引全掃描會讀取索引上的所有條目,下面幾種情況可能導致索引全掃描:
1)沒有條件但是所需獲取列的列表可以通過其中一列的索引來獲得;
select id from t3 DESCRIPTION OBJECT NAME ----------------------------------------------------------------------- SELECT STATEMENT, GOAL = ALL_ROWS INDEX FULL SCAN IDX_T3_ID
由於id列帶有索引,因此這裡優化器選擇了索引全掃描。
2)查詢條件中包含排序操作
select * from historyalarm order by queryid DESCRIPTION OBJECT NAME ----------------------------------------------------------------------- SELECT STATEMENT, GOAL = ALL_ROWS INDEX FULL SCAN IDX_HISTORYALARM$QUERYID
索引全掃描讀取單個數據塊,讀取每個條目的rowid,再通過rowid取出數據行,由於索引已經排序,所以不必執行排序操作。如果查詢只請求了索引列,數據庫將跳過表訪問,只通過訪問索引得到數據。
索引全掃描的另一個優勢在計算最大、最小值時:
select min(queryid) from historyalarm DESCRIPTION OBJECT NAME ----------------------------------------------------------------------- SELECT STATEMENT, GOAL = ALL_ROWS SORT AGGREGATE INDEX FULL SCAN (MIN/MAX) IDX_HISTORYALARM$QUERYID
由於索引本身已經排序,因此在計算最大最小值時只需要很小的代價。
當查詢條件中帶有符合索引中的列,但是不包含前導列時,就可能導致索引跳躍掃描。數據庫將一個復合索引拆分為多個邏輯子索引,符合索引前導列的不同值決定邏輯子索引的數量,即前導列的不同值越少,索引跳躍式掃描的性能就越好。
select value from t3 where value = 'test' DESCRIPTION OBJECT NAME ----------------------------------------------------------------------- SELECT STATEMENT, GOAL = ALL_ROWS INDEX SKIP SCAN IDX_T3_COMBINE
當索引本身包含查詢中指定的所有列時,Oracle執行索引快速全掃描。索引快速全掃描和索引全掃描的區別在於:索引全掃描使用單塊讀操作,而索引快速全掃描使用多塊讀。這種掃描不能用於避免排序,因為數據塊是通過無序的多塊讀取來讀取的。
select queryid from historyalarm DESCRIPTION OBJECT NAME ----------------------------------------------------------------------- SELECT STATEMENT, GOAL = ALL_ROWS INDEX FAST FULL SCAN IDX_HISTORYALARM$QUERYID