明理知意:複合索引優化及索引訪問原理
熊軍(老熊)
雲和恩墨西區總經理
Oracle ACED,ACOUG核心會員
這個案例發生在某天早上,執行在配置為128GB記憶體、64CPU的HP Superdome上的系統出現CPU佔用將近100%,執行佇列達到60~80,應用反應速度很慢的異常情況。
在使用者反映速度很慢後,檢查Oracle,發現很多的會話在等待latch free,latch#為98:
SQL> select * fromv$latchname where latch#=98; LATCH# NAME ---------- ---------------------------------------------------------------- 98 cache buffers chains
由於本章重點描述的是索引,關於“cache buffers chains latch”的等待,此處不做過多說明,這個latch的等待,通常情況下表明存在熱點塊,一般都是由於沒有正確使用索引、SQL所使用的索引選擇率不高引起。檢查正在等待latch free的會話正在執行的SQL,大部分都在執行類似於下面的SQL:
SELECT SUM(cnt), to_char(nvl(SUM(nvl(amount, 0)) /100, 0), ’FM9999999999990.90′) amount FROM (select count(payment_id) cnt,SUM(amount) amount from TABLE_A where staff_id = 12345 and CREATED_DATE >= trunc(sysdate) and state = ’C0C’ and operation_type in (’5KA’,’5KB’, ’5KC’, ’5KP’))
看起來這個SQL並不複雜,檢視其執行計劃:
PLAN_TABLE_OUTPUT ------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows |Bytes | Cost |Pstart |Pstop | ------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 26 | 125K | | | | 1 | SORT AGGREGATE | | 1 | 26 | | | | | 2 | VIEW | | 1 | 26 | 125K | | | | 3 | SORT AGGREGATE | | 1 | 30 | | | | |* 4 | TABLEACCESS BY GLOBAL INDEX ROWID |TABLE_ | 19675 | 576K | 125K | ROWID |ROW L | |* 5 | INDEX RANGE SCAN | IDX_A_3 | 1062K | | 3919 | | | ------------------------------------------------------------------------------------------- PredicateInformation (identified by operation id): --------------------------------------------------- 4 - filter(”TABLE_A”.”STAFF_ID”=12345 AND”TABLE_A”.”STATE”=’C0C’ AND (”TABLE_A”.”OPERATION_TYPE”=’5KA’ OR”TABLE_A”.”OPERATION_TYPE”=’5KB’ OR ”TABLE_A”.”OPERATION_TYPE”=’5KC’ OR”TABLE_A”.”OPERATION_TYPE”=’5KP’)) 5 -access(”TABLE_A”.”CREATED_DATE”>=TRUNC(SYSDATE@!)) Note: cpu costing is off
從中可以看到,Oracle評估出,利用索引掃描返回的行數高達100萬行,可想而知,由於選擇率過高,產生了大量的buffers chains latch爭用。
檢查PAYMENT表的索引:
SQL> select index_name,index_type from user_indexeswhere table_name=’TABLE_A’;
INDEX_NAME INDEX_TYPE
------------------------------ ---------------------------
IDX_A_1 NORMAL
IDX_A_2 NORMAL
IDX_A_3 NORMAL
IDX_A_4 NORMAL
IDX_A_5 NORMAL
IDX_A_6 NORMAL
IDX_A_7 NORMAL
IDX_A_8 NORMAL
PK_TABLE_A NORMAL
SQL> selectindex_name,column_name,column_position from user_ind_columns where table_name=’TABLE_A’order by 1,3;
INDEX_NAME COLUMN_NAME COLUMN_POSITION
------------------------------ ------------------------------ ---------------
IDX_A_1 SERIAL_NBR 1
IDX_A_2 A_ID 1
IDX_A_3 CREATED_DATE 1
IDX_A_4 METHOD 1
IDX_A_5 P_METHOD 1
IDX_A_6 S_ID 1
IDX_A_7 STAFF_ID 1
IDX_A_7 STATE_DATE 2
PK_TABLE_A TABLE_A_ID 1
以上輸出是對真正的輸出資訊加工處理後的結果。
由上可知,執行計劃中使用的索引IDX_A_3是在CREATED_DATE列上建立的單列索引。
這個SQL在之前沒有出現過類似問題,那問題在哪裡?
原來在當天凌晨做了一個大數量的業務操作,在TABLE_A中插入了大量的資料,因此用CREATED_DATE>=TRUNCATE(SYSDATE)這個條件時會從索引掃描中返回大量的行。而實際上回表之後用STAFF_ID和OPERATION_TYPE列上的條件過濾後的行數僅約2萬行(這是評估的資料,實際的資料遠遠比這個少)。很顯然,如果我們建立一個複合索引,那麼索引掃描返回的行數將大大減少,這樣也就大大減少了在表上訪問並進行過濾的資料量。
以STAFF_ID列為前導列與CREATE_DATE列一起建立複合索引後,系統馬上恢復正常。不過,有人會問,為什麼要使用STAFF_ID列做索引的前導列,而不用CREATE_DATE列做前導列?很多文件不是介紹說,複合索引要把選擇性最好的列放在最前面嗎?要回答這個問題,得首先了解索引的基本原理,包括Oracle資料庫對索引是如何儲存的、是怎樣通過索引來檢索索引資料的。
B Tree索引的結構及特點
Oracle資料庫中索引的儲存結構使用的是B Tree的一種變體,稱為B*Tree(B Star Tree),在資料庫中儲存資料以塊為單位,索引也不例外,資料庫中構建索引形成的BTree,與教科書中提到的B Tree有很明顯的差異。下面以圖11-1為例,介紹Oracle資料庫中B Tree索引的結構及其特點。
圖11-1 Oracle資料庫中B Tree索引的結構及其特點示意圖
圖11-1是一個簡單的B Tree索引示意圖,圖中虛線部分表示省略的部分。在介紹B Tree索引的特點之前,我們先來回顧一下資料結構中樹的幾個術語。
節點M的深度:從樹根節點到節點M的最短路徑長度。圖中根節點Root的深度為0,節點L1-1的深度為1,節點L0-1的深度為2。 節點M的層數:節點M的層數與其深度,實際上是相同的。 樹的高度:樹的深度值最大的那個節點,其深度+1即為樹的高度。比如圖中樹的高度為3。
Oracle資料庫的索引,有以下幾個特點:
- 儲存索引資料的塊,稱為B Tree樹的節點。有三種類型的節點,根(Root)節點、分枝(Branch)節點和葉(Leaf)節點。高度為1的索引,只有根節點,這個時候,索引只有唯一的一個葉節點,也同時是根節點,高度大於1時,根節點與分枝節點具有完全相同的結構,也就是說,這個時候的根節點,實際上也是一種分枝節點。分枝節點索引塊儲存的資料主要包括:索引值、鍵值對應的下一級節點塊地址、還有一個稱之為“kdxbrlmc”的指標,也就是如圖所示的“lmc”,這個指標就是比當前枝節點中最小的索引值還小的下一級節點塊的資料塊地址(DBA)。而葉節點索引塊儲存的資料主要是索引值以及對應的ROWID,和當前節點的前後兩個節點的資料塊地址。
- 索引的根節點,總是緊接在索引段頭的後面的一個數據塊。比如某個索引的段頭為7/7817(相對檔案號/塊號),那麼根節點塊就是7/7818。這個特點,是很少有文件提及的,但是這個小小的特點,其實非常重要。Oracle執行SQL時,直接從資料字典得到段頭位置後就能定位到根節點。這個特性使得就算是在小表上,使用索引也能減少邏輯讀,對於頻繁訪問的索引,特別是以INDEX UNIQUE SCAN方式訪問索引,所節省的邏輯讀是非常多的。
- 下面我們做一個簡單的測試,測試的資料庫版本為Linux AS4上的Oracle 10.2.0.4:
--建立一個只有2列、4行的表:
SQL> create tablet1 as select object_id,object_name from dba_objects where rownum<=4; Table created.
--建立一個非唯一索引:
SQL> create indext1_idx1 on t1(object_id);
Index created.
SQL> set autot onstat
SQL> colobject_name for a30
--全表掃描(Table Full Scan):
SQL> select /*+full(t1) */ * from t1 where object_id=28;
OBJECT_ID OBJECT_NAME ---------- ------------------------------ 28 CON$
Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
4 consistent gets
0 physical reads
0 redo size
478 bytes sent via SQL*Net to client
400 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
--索引範圍掃描(Index Range Scan):
SQL> select /*+index(t1) */ * from t1 where object_id=28;
OBJECT_ID OBJECT_NAME ---------- ------------------------------ 28 CON$
Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
3 consistent gets
0 physical reads
0 redo size
478 bytes sent via SQL*Net to client
400 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
SQL> set autotoff
--刪除索引,重新建立一個唯一索引:
SQL> drop indext1_idx1;
Index dropped.
SQL> createunique index t1_idx1 on t1(object_id);
Index created.
--索引唯一掃描(Index Unique Scan):
SQL> set autot onstat
SQL> select /*+ index(t1) */ * from t1 whereobject_id=28;
OBJECT_ID OBJECT_NAME ---------- ------------------------------ 28 CON$
Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
2 consistent gets
0 physical reads
0 redo size
478 bytes sent via SQL*Net to client
400 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
在上面的測試中,建立了一個只有2列,4行的表,這個表只佔用了1個數據塊的空間。對同樣的SQL,全表掃描、索引範圍掃描、索引唯一掃描3種不同的訪問方式,其邏輯讀各不相同:
注意在實際的測試中,每一個SQL應至少執行兩次,並以最後一次SQL執行後的邏輯讀等統計資料為準,因為在SQL解析時有遞迴呼叫,產生了其他的邏輯讀。
從上面的測試可以看到,對即使是很小的表,如果返回的資料量很小,使用索引都能夠減少邏輯讀,從而具有更好的效能。
- 索引是始終保持平衡的。這裡所說的平衡是指索引高度是保持平衡的,也就是從根節點到任意一個葉節點,其路徑都是等距的。比如圖11-1中,從“Root”到葉節點“L0-1”與“Root”到葉節點“L0-5”,都要訪問3個塊。可以說,這是B Tree索引最重要的一個特性。值得注意的是,在有的書和文章上面,提到B Tree索引不平衡,是指索引中的資料是傾斜的。如果某一個表刪除了大量的資料,會形成索引中很多的塊,只有很少量的資料甚至是空塊。比如圖11-1中,葉節點“L0-2”只有1條資料。這種情況常見於單向增長的列上的索引,比如Sequence、日期型別等,在刪除了大量資料後,由於列是單向增長的,除非是空塊,否則剩餘空間很難得到重用。
- 索引的每一個葉節點,有兩個指標,分別指向比當前節點最小索引值還小的葉節點塊地址,以及比當前節點最大索引值還大的葉節點塊地址。通過這兩個指標,把所有的葉節點串起來,形成一個雙向連結串列。在這個雙向連結串列上的所有索引值,從小到大排列,而對於倒序(Desc)索引,則是從大到小排列。值得注意的是,對於非唯一索引來說,每個值所對應的ROWID,也是索引值的一部分,所以在組成索引的各個列值均相等的情況下,會按ROWID為順序進行排序。
- 索引的分枝節點塊所儲存的索引值,並不是完整的索引值,而只是整個索引值的字首,只要能夠區分其大小就可以了。比如在前面的索引示意圖11-1中的“L1-1”分枝節點,有兩個值,AD和AK,其指向的葉節點起始索引值為ADK以及AKA,但是其字首AD和AK即可以區分其大小。這種設計,能夠使分枝節點儲存更多的條目,減少了分枝節點數,特別是在多列複合索引中,對於很大的表,甚至可以減少B Tree樹的高度。
- B Tree索引不對NULL值進行索引,對於某一行,索引的所有列的值都是NULL值時,該行不能被索引。不過Cluster Index是可以對NULL值進行索引的,但是本文主要是討論普通表上的B Tree索引,對Cluster Index不做討論。由於Oracle索引的這個特性,使得IS NULL這種條件的SQL不能夠使用索引。但是我們可以通過建複合索引的形式來使這種SQL也能夠使用索引。