文章轉自:http://www.cnblogs.com/adslg/archive/2012/06/23/2559206.html 感謝作者的分享
摘要:本文對B樹索引的結構、內部管理等方面做了一個全面的介紹。同時深入探討了一些與B樹索引有關的廣爲流傳的說法,比如刪除記錄對索引的影響,定期重建索引能解決許多性能問題等。
1.B樹索引的相關概念
索引與表一樣,也屬於段(segment)的一種。裏面存放了用戶的數據,跟表一樣需要佔用磁盤空間。只
不過,在索引裏的數據存放形式與表裏的數據存放形式非常的不一樣。在理解索引時,可以想象一本書,其中書的內容就相當於表裏的數據,而書前面的目錄就相當於該表的索引。同時,通常情況下,索引所佔用的磁盤空間要比表要小的多,其主要作用是爲了加快對數據的搜索速度,也可以用來保證數據的唯一性。
但是,索引作爲一種可選的數據結構,你可以選擇爲某個表裏的創建索引,也可以不創建。這是因爲一旦創建了索引,就意味着oracle對錶進行DML(包括INSERT、UPDATE、DELETE)時,必須處理額外的工作量(也就是對索引結構的維護)以及存儲方面的開銷。所以創建索引時,需要考慮創建索引所帶來的查詢性能方面的提高,與引起的額外的開銷相比,是否值得。
從物理上說,索引通常可以分爲:分區和非分區索引、常規B樹索引、位圖(bitmap)索引、翻轉(reverse)索引等。其中,B樹索引屬於最常見的索引,由於我們的這篇文章主要就是對B樹索引所做的探討,因此下面只要說到索引,都是指B樹索引。
B樹索引是一個典型的樹結構,其包含的組件主要是:
1) 葉子節點(Leaf node):包含條目直接指向表裏的數據行。
2) 分支節點(Branch node):包含的條目指向索引裏其他的分支節點或者是葉子節點。
3) 根節點(Root node):一個B樹索引只有一個根節點,它實際就是位於樹的最頂端的分支節點。
可以用下圖一來描述B樹索引的結構。其中,B表示分支節點,而L表示葉子節點。
對於分支節點塊(包括根節點塊)來說,其所包含的索引條目都是按照順序排列的(缺省是升序排列,也可以在創建索引時指定爲降序排列)。每個索引條目(也可以叫做每條記錄)都具有兩個字段。第一個字段表示當前該分支節點塊下面所鏈接的索引塊中所包含的最小鍵值;第二個字段爲四個字節,表示所鏈接的索引塊的地址,該地址指向下面一個索引塊。在一個分支節點塊中所能容納的記錄行數由數據塊大小以及索引鍵值的長度決定。比如從上圖一可以看到,對於根節點塊來說,包含三條記錄,分別爲(0 B1)、(500 B2)、(1000 B3),它們指向三個分支節點塊。其中的0、500和1000分別表示這三個分支節點塊所鏈接的鍵值的最小值。而B1、B2和B3則表示所指向的三個分支節點塊的地址。
對於葉子節點塊來說,其所包含的索引條目與分支節點一樣,都是按照順序排列的(缺省是升序排列,也可以在創建索引時指定爲降序排列)。每個索引條目(也可以叫做每條記錄)也具有兩個字段。第一個字段表示索引的鍵值,對於單列索引來說是一個值;而對於多列索引來說則是多個值組合在一起的。第二個字段表示鍵值所對應的記錄行的ROWID,該ROWID是記錄行在表裏的物理地址。如果索引是創建在非分區表上或者索引是分區表上的本地索引的話,則該ROWID佔用6個字節;如果索引是創建在分區表上的全局索引的話,則該ROWID佔用10個字節。
知道這些信息以後,我們可以舉個例子來說明如何估算每個索引能夠包含多少條目,以及對於表來說,所產生的索引大約多大。對於每個索引塊來說,缺省的PCTFREE爲10%,也就是說最多隻能使用其中的90%。同時9i以後,這90%中也不可能用盡,只能使用其中的87%左右。也就是說,8KB的數據塊中能夠實際用來存放索引數據的空間大約爲6488(8192×90%×88%)個字節。
假設我們有一個非分區表,表名爲warecountd,其數據行數爲130萬行。該表中有一個列,列名爲goodid,其類型爲char(8),那麼也就是說該goodid的長度爲固定值:8。同時在該列上創建了一個B樹索引。
在葉子節點中,每個索引條目都會在數據塊中佔一行空間。每一行用2到3個字節作爲行頭,行頭用來存放標記以及鎖定類型等信息。同時,在第一個表示索引的鍵值的字段中,每一個索引列都有1個字節表示數據長度,後面則是該列具體的值。那麼對於本例來說,在葉子節點中的一行所包含的數據大致如下圖二所示:
從上圖可以看到,在本例的葉子節點中,一個索引條目佔18個字節。同時我們知道8KB的數據塊中真正可以用來存放索引條目的空間爲6488字節,那麼在本例中,一個數據塊中大約可以放360(6488/18)個索引條目。而對於我們表中的130萬條記錄來說,則需要大約3611(1300000/360)個葉子節點塊。
而對於分支節點裏的一個條目(一行)來說,由於它只需保存所鏈接的其他索引塊的地址即可,而不需要保存具體的數據行在哪裏,因此它所佔用的空間要比葉子節點要少。分支節點的一行中所存放的所鏈接的最小鍵值所需空間與上面所描述的葉子節點相同;而存放的索引塊的地址只需要4個字節,比葉子節點中所存放的ROWID少了2個字節,少的這2個字節也就是ROWID中用來描述在數據塊中的行號所需的空間。因此,本例中在分支節點中的一行所包含的數據大致如下圖三所示:
2. B樹索引的內部結構
我們可以使用如下方式將B樹索引轉儲成樹狀結構的形式而呈現出來:
alter session set events 'immediate trace name treedump level INDEX_OBJECT_ID';
比如,對於上面的例子來說,我們把創建在goodid上的名爲idx_warecountd_goodid的索引轉儲出來。
SQL> select object_id from user_objects where object_name='IDX_WARECOUNTD_GOODID';
OBJECT_ID
----------
7378
SQL> alter session set events 'immediate trace name treedump level 7378';
打開轉儲出來的文件以後,我們可以看到類似下面的內容:
----- begin tree dump
branch: 0x180eb0a 25225994 (0: nrow: 9, level: 2)
branch: 0x180eca1 25226401 (-1: nrow: 405, level: 1)
leaf: 0x180eb0b 25225995 (-1: nrow: 359 rrow: 359)
leaf: 0x180eb0c 25225996 (0: nrow: 359 rrow: 359)
leaf: 0x180eb0d 25225997 (1: nrow: 359 rrow: 359)
leaf: 0x180eb0e 25225998 (2: nrow: 359 rrow: 359)
…………………
branch: 0x180ee38 25226808 (0: nrow: 406, level: 1)
leaf: 0x180eca0 25226400 (-1: nrow: 359 rrow: 359)
leaf: 0x180eca2 25226402 (0: nrow: 359 rrow: 359)
leaf: 0x180eca3 25226403 (1: nrow: 359 rrow: 359)
leaf: 0x180eca4 25226404 (2: nrow: 359 rrow: 359)
…………………
其中,每一行的第一列表示節點類型:branch表示分支節點(包括根節點),而leaf則表示葉子節點;第二列表示十六進制表示的節點的地址;第三列表示十進制表示的節點的地址;第四列表示相對於前一個節點的位置,根節點從0開始計算,其他分支節點和葉子節點從-1開始計算;第五列的nrow表示當前節點中所含有的索引條目的數量。比如我們可以看到根節點中含有的nrow爲9,表示根節點中含有9個索引條目,分別指向9個分支節點;第六列中的level表示分支節點的層級,對於葉子節點來說level都是0。第六列中的rrow表示有效的索引條目(因爲索引條目如果被刪除,不會立即被清除出索引塊中。所以nrow減rrow的數量就表示已經被刪除的索引條目數量)的數量,比如對於第一個leaf來說,其rrow爲359,也就是說該葉子節點中存放了359個可用索引條目,分別指向表warecountd的359條記錄。
上面這種方式以樹狀形式轉儲整個索引。同時,我們可以轉儲一個索引節點來看看其中存放了些什麼。轉儲的方式爲:
alter system dump datafile file# block block#;
我們從上面轉儲結果中的第二行知道,索引的根節點的地址爲25225994,因此我們先將其轉換爲文件號以及數據塊號。
SQL> select dbms_utility.data_block_address_file(25225994),
2 dbms_utility.data_block_address_block(25225994) from dual;
DBMS_UTILITY.DATA_BLOCK_ADDRES DBMS_UTILITY.DATA_BLOCK_ADDRES
------------------------------ ------------------------------
6 60170
於是,我們轉儲根節點的內容。
SQL> alter system dump datafile 6 block 60170;
打開轉儲出來的跟蹤文件,我們可以看到如下的索引頭部的內容:
header address 85594180=0x51a1044
kdxcolev 2
KDXCOLEV Flags = - - -
kdxcolok 0
kdxcoopc 0x80: pcode=0: iot flags=--- is converted=Y
kdxconco 2
kdxcosdc 0
kdxconro 8
kdxcofbo 44=0x2c
kdxcofeo 7918=0x1eee
kdxcoavs 7874
kdxbrlmc 25226401=0x180eca1
kdxbrsno 0
kdxbrbksz 8060
其中的kdxcolev表示索引層級號,這裏由於我們轉儲的是根節點,所以其層級號爲2。對葉子節點來說該值爲0;kdxcolok表示該索引上是否正在發生修改塊結構的事務;kdxcoopc表示內部操作代碼;kdxconco表示索引條目中列的數量;kdxcosdc表示索引結構發生變化的數量,當你修改表裏的某個索引鍵值時,該值增加;kdxconro表示當前索引節點中索引條目的數量,但是注意,不包括kdxbrlmc指針;kdxcofbo表示當前索引節點中可用空間的起始點相對當前塊的位移量;kdxcofeo表示當前索引節點中可用空間的最尾端的相對當前塊的位移量;kdxcoavs表示當前索引塊中的可用空間總量,也就是用kdxcofeo減去kdxcofbo得到的。kdxbrlmc表示分支節點的地址,該分支節點存放了索引鍵值小於row#0(在轉儲文檔後半部分顯示)所含有的最小值的所有節點信息;kdxbrsno表示最後一個被修改的索引條目號,這裏看到是0,表示該索引是新建的索引;kdxbrbksz表示可用數據塊的空間大小。實際從這裏已經可以看到,即便是PCTFREE設置爲0,也不能用足8192字節。
再往下可以看到如下的內容。這部分內容就是在根節點中所記錄的索引條目,總共是8個條目。再加上
row#0[8043] dba: 25226808=0x180ee38
col 0; len 8; (8): 31 30 30 30 30 33 39 32
col 1; len 3; (3): 01 40 1a
……
row#7[7918] dba: 25229599=0x180f91f
col 0; len 8; (8): 31 30 30 31 31 32 30 33
col 1; len 4; (4): 01 40 8f a5
kdxbrlmc所指向的第一個分支節點,我們知道該根節點中總共存放了9個分支節點的索引條目,而這正是我們在前面所指出的爲了管理3611個葉子節點,我們需要9個分支節點。
每個索引條目都指向一個分支節點。其中col 1表示所鏈接的分支節點的地址,該值經過一定的轉換以後實際就是row#所在行的dba的值。如果根節點下沒有其他的分支節點,則col 1爲TERM;col 0表示該分支節點所鏈接的最小鍵值。其轉換方式非常複雜,比如對於row #0來說,col 0爲31 30 30 30 30 30 30 33,則將其中每對值都使用函數to_number(NN,’XX’)的方式從十六進制轉換爲十進制,於是我們得到轉換後的值:49,48,48,48,48,48,48,51,因爲我們已經知道索引鍵值是char類型的,所以對每個值都運用chr函數就可以得到被索引鍵值爲:10000003。實際上,對10000003運用dump函數得到的結果就是:49,48,48,48,48,48,48,51。所以我們也就知道,10000003就是dba爲25226808的索引塊所鏈接的最小鍵值。
SQL> select dump('10000003') from dual;
DUMP('10000003')
-------------------------------------
Typ=96 Len=8: 49,48,48,48,48,48,48,50
接下來,我們從根節點中隨便找一個分支節點,假設就是row#0所描述的25226808。對其運用前面所介紹過的dbms_utility裏的存儲過程獲得其文件號和數據塊號,並對該數據塊進行轉儲,其內容如下所示。可以
row#0[8043] dba: 25226402=0x180eca2
col 0; len 8; (8): 31 30 30 30 30 33 39 33
col 1; len 3; (3): 01 40 2e
………
row#404[853] dba: 25226806=0x180ee36
col 0; len 8; (8): 31 30 30 30 31 36 34 30
col 1; len 3; (3): 01 40 09
----- end of branch block dump -----
發現內容與根節點完全類似,只不過該索引塊中所包含的索引條目(指向葉子節點)的數量更多了,爲405個。這也與我們前面所說的一個分支索引塊可以存放大約405(6488/16)個索引條目完全一致。
然後,我們從中隨便挑一個葉子節點,對其進行轉儲。假設就選row#0行所指向的葉子節點,根據dba的值:25226402可以知道,文件號爲6,數據塊號爲60578。將其轉儲以後,其內容如下所示,我只顯示與分支節點不同的部分。
………
kdxlespl 0
kdxlende 0
kdxlenxt 25226403=0x180eca3
kdxleprv 25226400=0x180eca0
kdxledsz 0
kdxlebksz 8036
其中的kdxlespl表示當葉子節點被拆分時未提交的事務數量;kdxlende表示被刪除的索引條目的數量;kdxlenxt表示當前葉子節點的下一個葉子節點的地址;kdxlprv表示當前葉子節點的上一個葉子節點的地址;kdxledsz表示可用空間,目前是0。
轉儲文件中接下來的部分就是索引條目部分,每個條目包含一個ROWID,指向一個表裏的數據行。如下所示。其中flag表示標記,比如刪除標記等;而lock表示鎖定信息。col 0表示索引鍵值,其算法與我們在前面介紹分支節點時所說的算法一致。col 1表示ROWID。我們同樣可以看到,該葉子節點中包含了359個索引條目,與我們前面所估計的一個葉子節點中大約可以放360個索引條目也是基本一致的。
row#0[8018] flag: -----, lock: 0
col 0; len 8; (8): 31 30 30 30 30 33 39 33
col 1; len 6; (6): 01 40 2e 93 00 16
row#1[8000] flag: -----, lock: 0
col 0; len 8; (8): 31 30 30 30 30 33 39 33
col 1; len 6; (6): 01 40 2e e7 00 0e
…………
row#358[1574] flag: -----, lock: 0
col 0; len 8; (8): 31 30 30 30 30 33 39 37
col 1; len 6; (6): 01 40 18 ba 00 1f
----- end of leaf block dump -----
3. B樹索引的訪問
我們已經知道了B樹索引的體系結構,那麼當oracle需要訪問索引裏的某個索引條目時,oracle是如何找
到該索引條目所在的數據塊的呢?
當oracle進程需要訪問數據文件裏的數據塊時,oracle會有兩種類型的I/O操作方式:
1) 隨機訪問,每次讀取一個數據塊(通過等待事件“db file sequential read”體現出來)。
2) 順序訪問,每次讀取多個數據塊(通過等待事件“db file scattered read”體現出來)。
第一種方式則是訪問索引裏的數據塊,而第二種方式的I/O操作屬於全表掃描。這裏順帶有一個問題,爲
何隨機訪問會對應到db file sequential read等待事件,而順序訪問則會對應到db file scattered read等待事件呢?這似乎反過來了,隨機訪問才應該是分散(scattered)的,而順序訪問才應該是順序(sequential)的。其實,等待事件主要根據實際獲取物理I/O塊的方式來命名的,而不是根據其在I/O子系統的邏輯方式來命名的。下面對於如何獲取索引數據塊的方式中會對此進行說明。
我們看到前面對B樹索引的體系結構的描述,可以知道其爲一個樹狀的立體結構。其對應到數據文件裏的
排列當然還是一個平面的形式,也就是像下面這樣。因此,當oracle需要訪問某個索引塊的時候,勢必會在這個結構上跳躍的移動。
/根/分支/分支/葉子/…/葉子/分支/葉子/葉子/…/葉子/分支/葉子/葉子/…/葉子/分支/.....
當oracle需要獲得一個索引塊時,首先從根節點開始,根據所要查找的鍵值,從而知道其所在的下一層的分支節點,然後訪問下一層的分支節點,再次同樣根據鍵值訪問再下一層的分支節點,如此這般,最終訪問到最底層的葉子節點。可以看出,其獲得物理I/O塊時,是一個接着一個,按照順序,串行進行的。在獲得最終物理塊的過程中,我們不能同時讀取多個塊,因爲我們在沒有獲得當前塊的時候是不知道接下來應該訪問哪個塊的。因此,在索引上訪問數據塊時,會對應到db file sequential read等待事件,其根源在於我們是按照順序從一個索引塊跳到另一個索引塊,從而找到最終的索引塊的。
那麼對於全表掃描來說,則不存在訪問下一個塊之前需要先訪問上一個塊的情況。全表掃描時,oracle知道要訪問所有的數據塊,因此唯一的問題就是儘可能高效的訪問這些數據塊。因此,這時oracle可以採用同步的方式,分幾批,同時獲取多個數據塊。這幾批的數據塊在物理上可能是分散在表裏的,因此其對應到db file scattered read等待事件。
4. B樹索引的管理機制
4.1 B樹索引的對於插入(INSERT)的管理
對於B樹索引的插入情況的描述,可以分爲兩種情況:一種是在一個已經充滿了數據的表上創建索引時,
索引是怎麼管理的;另一種則是當一行接着一行向表裏插入或更新或刪除數據時,索引是怎麼管理的。
對於第一種情況來說,比較簡單。當在一個充滿了數據的表上創建索引(create index命令)時,oracle會先掃描表裏的數據並對其進行排序,然後生成葉子節點。生成所有的葉子節點以後,根據葉子節點的數量生成若干層級的分支節點,最後生成根節點。這個過程是很清晰的。
但是對於第二種情況來說,會複雜很多。我們結合一個例子來說明。爲了方便起見,我們在一個數據塊爲2KB的表空間上創建一個測試表,併爲該表創建一個索引,該索引同樣位於2KB的表空間上。
SQL> create table index_test(id char(150)) tablespace tbs_2k;
SQL> create index idx_test on index_test(id) tablespace tbs_2k;
當一開始在一個空的表上創建索引的時候,該索引沒有根節點,只有一個葉子節點。我們以樹狀形式轉儲上面的索引idx_test。
SQL> select object_id from user_objects where object_name='IDX_TEST';
OBJECT_ID
----------
7390
SQL> alter session set events 'immediate trace name treedump level 7390';
從轉儲文件可以看到,該索引中只有一個葉子節點(leaf)。
----- begin tree dump
leaf: 0x1c001a2 29360546 (0: nrow: 0 rrow: 0)
----- end tree dump
隨着數據不斷被插入表裏,該葉子節點中的索引條目也不斷增加,當該葉子節點充滿了索引條目而不能再放下新的索引條目時,該索引就必須擴張,必須再獲取一個可用的葉子節點。這時,索引就包含了兩個葉子節點,但是兩個葉子節點不可能單獨存在的,這時它們兩必須有一個上級的分支節點,其實這也就是根節點了。於是,現在,我們的索引應該具有3個索引塊,一個根節點,兩個葉子節點。
我們來做個試驗看看這個過程。我們先試着插入插入10條記錄。注意,對於2KB的索引塊同時PCTFREE爲缺省的10%來說,只能使用其中大約1623字節(2048×90%×88%)。對於表index_test來說,葉子節點中的每個索引條目所佔的空間大約爲161個字節(3個字節行頭+1個字節列長+150個字節列本身+1個字節列長+6個字節ROWID),那麼當我們插入將10條記錄以後,將消耗掉大約1610個字節。
SQL> begin
2 for i in 1..10 loop
3 insert into index_test values (rpad(to_char(i*2),150,'a'));
4 end loop;
5 end;
6 /
SQL> commit;
SQL> select file_id,block_id,blocks from dba_extents where segment_name='IDX_TEST';
FILE_ID BLOCK_ID BLOCKS
---------- ---------- ----------
7 417 32
SQL> alter system dump datafile 7 block 418; --因爲第一個塊爲塊頭,不含數據,所以轉儲第二個塊。
打開跟蹤文件以後,如下所示,可以發現418塊仍然是一個葉子節點,包含10個索引條目,該索引塊還沒有被拆分。注意其中的kdxcoavs爲226,說明可用空間還剩226個字節,說明還可以插入一條記錄。之所以與前面計算出來的只能放10條記錄有出入,是因爲可用的1623字節只是一個估計值。
……
kdxcoavs 226
……
row#0[1087] flag: -----, lock: 0
col 0; len 150; (150):
31 30 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61
61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61
61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61
61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61
61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61
61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61
col 1; len 6; (6): 01 c0 01 82 00 04
row#1[926] flag: -----, lock: 0
……
接下來,我們再次插入一條記錄,以便基本充滿該葉子節點,使得剩下的可用空間不足以再插入一條新的條目。如下所示。
SQL> insert into index_test values(rpad(to_char(11*2),150,'a'));
這個時候我們再次轉儲418塊以後會發現與前面轉儲的內容基本一致,只是又增加了一個索引條目。而這個時候,如果向表裏再次插入一條新的記錄的話,該葉子節點(418塊)必須進行拆分。
SQL> insert into index_test values(rpad(to_char(12*2),150,'a'));
SQL> alter system dump datafile 7 block 418;
轉儲出418塊以後,我們會發現,該索引塊從葉子節點變成了根節點(kdxcolev爲1,同時row#0部分的col 1爲TERM表示根節點下沒有其他分支節點)。這也就說明,當第一個葉子節點充滿以後,進行分裂時,先獲得兩個可用的索引塊作爲新的葉子節點,然後將當前該葉子節點裏所有的索引條目拷貝到這兩個新獲得的葉子節點,最後將原來的葉子節點改變爲根節點。
……
kdxcolev 1
……
kdxbrlmc 29360547=0x1c001a3
……
row#0[1909] dba: 29360548=0x1c001a4
col 0; len 1; (1): 34
col 1; TERM
----- end of branch block dump -----
同時,從上面的kdxbrlmc和row#0中的dba可以知道,該根節點分別指向29360547和29360548兩個葉子節點。我們分別對這兩個葉子節點進行轉儲看看裏面放了些什麼。
SQL> select dbms_utility.data_block_address_file(29360547),
2 dbms_utility.data_block_address_block(29360547) from dual;
DBMS_UTILITY.DATA_BLOCK_ADDRES DBMS_UTILITY.DATA_BLOCK_ADDRES
------------------------------ ------------------------------
7 419
SQL> select dbms_utility.data_block_address_file(29360548),
2 dbms_utility.data_block_address_block(29360548) from dual;
DBMS_UTILITY.DATA_BLOCK_ADDRES DBMS_UTILITY.DATA_BLOCK_ADDRES
------------------------------ ------------------------------
7 420
SQL> alter system dump datafile 7 block 419;
SQL> alter system dump datafile 7 block 420;
在打開跟蹤文件之前,我們先來看看錶index_test裏存放了哪些數據。
SQL> select substr(id,1,2) from index_test order by substr(id,1,2);
SUBSTR(ID,1,2)
--------------
10
12
14
16
18
20
22
24
2a
4a
6a
8a
打開419塊的跟蹤文件可以發現,裏面存放了10、12、14、16、18、20、22、24和2a;而420塊的跟蹤文件中記錄了4a、6a和8a。也就是說,由於最後我們插入24的緣故,導致整個葉子節點發生分裂,從而將10、12、14、16、18、20、22、和2a放到419塊裏,而4a、6a和8a則放入420塊裏。然後,再將新的索引條目(24)插入對應的索引塊裏,也就是419塊。
假如我們再最後不是插入12*2,而是插入9會怎麼樣?我們重新測試一下,返回到index_test裏有11條記錄的情況下,然後我們再插入9。
SQL> insert into index_test values (rpad('9',150,'a'));
這個時候,418塊還是和原來一樣變成了根節點,同時仍然生成出了2個葉子節點塊,分別是419和420。但是有趣的是,419塊裏的內容與在插入9之前的葉子節點(當時的418塊)的內容完全相同,而420塊裏則只有一個索引條目,也就是新插入的9。這也就是說,由於最後我們插入9的緣故,導致整個葉子節點發生分裂。但是分裂過程與插入12*2的情況是不一樣的,這時該葉子節點的內容不進行拆分,而是直接完全拷貝到一個新的葉子節點(419)裏,然後將新插入的9放入另外一個新的葉子節點(420)。我們應該注意到,插入的這個9表裏所有記錄裏的最大字符串。
如果這時,我們再次插入12*2,則會發現419號節點的分裂過程和前面描述的一樣,會將原來放在419塊裏的4a、6a和8a放入一個新的葉子節點裏(421塊),然後將12*2放入419塊,於是這個時候419塊所含有的索引條目爲10、12、14、16、18、20、22、和2a。同時420塊沒有發生變化。
根據上面的測試結果,我們可以總結一下葉子節點的拆分過程。這個過程需要分成兩種情況,一種是插入的鍵值不是最大值;另一種是插入的鍵值是最大值。
對於第一種情況來說,當一個非最大鍵值要進入索引,但是發現所應進入的索引塊不足以容納當前鍵值時:
1) 從索引可用列表上獲得一個新的索引數據塊。
2) 將當前充滿了的索引中的索引條目分成兩部分,一部分是具有較小鍵值的,另一部分是具有較大鍵值的。Oracle會將具有較大鍵值的部分移入新的索引數據塊,而較小鍵值的部分保持不動。
3) 將當前鍵值插入合適的索引塊中,可能是原來空間不足的索引塊,也可能是新的索引塊。
4) 更新原來空間不足的索引塊的kdxlenxt信息,使其指向新的索引塊。
5) 更新位於原來空間不足的索引塊右邊的索引塊裏的kdxleprv,使其指向新的索引塊。
6) 向原來空間不足的索引塊的上一級的分支索引塊中添加一個索引條目,該索引條目中保存新的索引塊裏的最小鍵值,以及新的索引塊的地址。
從上面有關葉子節點分裂的過程可以看出,其過程是非常複雜的。因此如果發生的是第二種情況,則爲了
簡化該分裂過程,oracle省略了上面的第二步,而是直接進入第三步,將新的鍵值插入新的索引塊中。
在上例中,當葉子節點越來越多,導致原來的根節點不足以存放新的索引條目(這些索引條目指向葉子節點)時,則該根節點必須進行分裂。當根節點進行分裂時:
1) 從索引可用列表上獲得兩個新的索引數據塊。
2) 將根節點中的索引條目分成兩部分,這兩部分分別放入兩個新的索引塊,從而形成兩個新的分支節點。
3) 更新原來的根節點的索引條目,使其分別指向這兩個新的索引塊。
因此,這時的索引層次就變成了2層。同時可以看出,根節點索引塊在物理上始終都是同一個索引塊。而
隨着數據量的不斷增加,導致分支節點又要進行分裂。分支節點的分裂過程與根節點類似(實際上根節點分裂其實是分支節點分裂的一個特例而已):
1) 從索引可用列表上獲得一個新的索引數據塊。
2) 將當前滿了的分支節點裏的索引條目分成兩部分,較小鍵值的部分不動,而較大鍵值的部分移入新的索引塊。
3) 將新的索引條目插入合適的分支索引塊。
4) 在上層分支索引塊中添加一個新的索引條目,使其指向新加的分支索引塊。
當數據量再次不斷增加,導致原來的根節點不足以存放新的索引條目(這些索引條目指向分支節點)時,
再次引起根節點的分裂,其分裂過程與前面所說的由於葉子節點的增加而導致的根節點分裂的過程是一樣的。
同時,根節點分裂以後,索引的層級再次遞增。由此可以看出,根據B樹索引的分裂機制,一個B樹索引始終都是平衡的。注意,這裏的平衡是指每個葉子節點與根節點的距離都是相同的。同時,從索引的分裂機制可以看出,當插入的鍵值始終都是增大的時候,索引總是向右擴展;而當插入的鍵值始終都是減小的時候,索引則總是向左擴展。
4.2 B樹索引的對於刪除(DELETE)的管理
上面介紹了有關插入鍵值時索引的管理機制,那麼對於刪除鍵值時會怎麼樣呢?
在介紹刪除索引鍵值的機制之前,先介紹與索引相關的一個比較重要的視圖:index_stats。該視圖顯示了
大量索引內部的信息,該視圖正常情況下沒有數據,只有在運行了下面的命令以後纔會被填充數據,而且該視圖中只能存放一條與分析過的索引相關的記錄,不會有第二條記錄。同時,也只有運行了該命令的session才能夠看到該視圖裏的數據,其他session不能看到其中的數據。
analyze index INDEX_NAME validate structure;
不過要注意一點,就是該命令有一個壞處,就是在運行過程中,會鎖定整個表,從而阻塞其他session對錶進行插入、更新和刪除等操作。這是因爲該命令的主要目的並不是用來填充index_stats視圖的,其主要作用在於校驗索引中的每個有效的索引條目都對應到表裏的一行,同時表裏的每一行數據在索引中都存在一個對應的索引條目。爲了完成該目的,所以在運行過程中要鎖定整個表,同時對於很大的表來說,運行該命令需要耗費非常多的時間。
在視圖index_stats中,height表示B樹索引的高度;blocks表示分配了的索引塊數,包括還沒有被使用的;pct_used表示當前索引中被使用了的空間的百分比。其值是通過該視圖中的(used_space/btree_space)*100計算而來。used_space表示已經使用的空間,而btree_space表示索引所佔的總空間;del_lf_rows表示被刪除的記錄行數(表裏的數據被刪除並不會立即將其對應於索引裏的索引條目清除出索引塊,我們後面會說到);del_lf_rows_len表示被刪除的記錄所佔的總空間;lf_rows表示索引中包含的總記錄行數,包括已經被刪除的記錄行數。這樣的話,索引中未被刪除的記錄行數就是lf_rows-del_lf_rows。同時我們可以計算未被刪除的記錄所對應的索引條目(也就是有效索引條目)所佔用的空間爲((used_space – del_lf_rows_len) / btree_space) * 100。
然後,我們還是接着上個例子(最後插入了12*2的例子)來測試一下。這時我們已經知道,該例中的索引具有兩個葉子節點,一個葉子節點(塊號爲419)包含10、12、14、16、18、20、22、24和2a,而另一個葉子節點(塊號爲420)包含4a、6a和8a。我們插入41、42、43、44、45、46、47和48各8條記錄,這時可以知道這8條記錄所對應的索引條目將會進入索引塊420中,從而該塊420被充滿。
SQL> begin
2 for i in 1..8 loop
3 insert into index_test values (rpad('4'||to_char(i),150,'a'));
4 end loop;
5 end;
6 /
我們先分析索引從而填充index_stats視圖。
SQL> analyze index idx_test validate structure;
SQL> select LF_ROWS,DEL_LF_ROWS,DEL_LF_ROWS_LEN,USED_SPACE,BTREE_SPACE from index_stats;
LF_ROWS DEL_LF_ROWS DEL_LF_ROWS_LEN USED_SPACE BTREE_SPACE
---------- ----------- --------------- ---------- -----------
20 0 0 3269 5600
從上面視圖可以看到,當前索引共20條記錄,沒有被刪除的記錄,共使用了3269個字節。
然後我們刪除位於索引塊419裏的索引條目,包括10、12、14、16各4條記錄。
SQL> delete index_test where substr(id,1,2) in('10','12','14','16');
SQL> commit;
SQL> alter system dump datafile 7 block 419;
打開轉儲出來的文件可以發現如下的內容(我們節選了部分關鍵內容)。可以發現,kdxconro爲3,說明該索引節點裏還有9個索引條目。所以說,雖然表裏的數據被刪除了,但是對應的索引條目並沒有被刪除,只是在各個索引條目上(row#一行中的flag爲D)做了一個D的標記,表示該索引條目被delete了。
kdxconro 9
row#0[443] flag: ---D-, lock: 2
row#1[604] flag: ---D-, lock: 2
row#2[765] flag: ---D-, lock: 2
row#3[926] flag: ---D-, lock: 2
然後,我們再以樹狀結構轉儲索引,打開樹狀轉儲跟蹤文件可以看到如下內容。可以知道,塊419裏包含9個索引條目(nrow爲9),而有效索引條目只有5個(rrow爲5),那麼被刪除了的索引條目就是4個(9減5)。
SQL> alter session set events 'immediate trace name treedump level 7390';
----- begin tree dump
branch: 0x1c001a2 29360546 (0: nrow: 2, level: 1)
leaf: 0x1c001a3 29360547 (-1: nrow: 9 rrow: 5)
leaf: 0x1c001a4 29360548 (0: nrow: 11 rrow: 11)
----- end tree dump
這時,我們再次分析索引,填充index_stats視圖。
SQL> analyze index idx_test validate structure;
SQL> select LF_ROWS,DEL_LF_ROWS,DEL_LF_ROWS_LEN,USED_SPACE,BTREE_SPACE from index_stats;
LF_ROWS DEL_LF_ROWS DEL_LF_ROWS_LEN USED_SPACE BTREE_SPACE
---------- ----------- --------------- ---------- -----------
20 4 652 3269 5600
對照刪除之前視圖裏的信息,很明顯看到,當前索引仍然爲20條記錄,但是其中有4條爲刪除的,但是索引所使用的空間並沒有釋放被刪除記錄所佔用的652個字節,仍然爲刪除之前的3269個字節。這也與轉儲出來的索引塊的信息一致。
接下來,我們測試這個時候插入一條記錄時,索引會怎麼變化。分三種情況進行插入:第一種是插入一個屬於原來被刪除鍵值範圍內的值,比如13,觀察其會如何進入包含設置了刪除標記的索引塊;第二種是插入原來被刪除的鍵值中的一個,比如16,觀察其是否能夠重新使用原來的索引條目;第三種是插入一個完全不屬於該表中已有記錄的範圍的值,比如rpad('M',150,'M'),觀察其對塊419以及420會產生什麼影響。
我們測試第一種情況:
SQL> insert into index_test values (rpad(to_char(13),150,'a'));
SQL> alter system dump datafile 7 block 419;
打開跟蹤文件以後會發現419塊裏的內容發生了變化,如下所示。我們可以發現一個很有趣的現象,從kdxconro爲6說明插入了鍵值13以後,導致原來四個被標記爲刪除的索引條目都被清除出了索引塊。同時,我們也確實發現原來標記爲D的四個索引條目都消失了。
……
kdxconro 6
……
kdxlende 0
……
row#0[121] flag: -----, lock: 2 被插入13
col 0; len 150; (150):
31 33 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61
……
我們分析索引,看看index_stats視圖會如何變化。
SQL> analyze index idx_test validate structure;
SQL> select LF_ROWS,DEL_LF_ROWS,DEL_LF_ROWS_LEN,USED_SPACE,BTREE_SPACE from index_stats;
LF_ROWS DEL_LF_ROWS DEL_LF_ROWS_LEN USED_SPACE BTREE_SPACE
---------- ----------- --------------- ---------- -----------
17 0 0 2780 5600
很明顯,原來的del_lf_rows從4變爲了0,同時used_space也從原來的3269變成了2780。表示原來被刪除的索引條目所佔用的空間已經釋放了。
我們繼續測試第二種情況:
SQL> insert into index_test values (rpad(to_char(8*2),150,'a'));
SQL> alter system dump datafile 7 block 419;
打開跟蹤文件以後,發現對於插入已經被標記爲刪除的記錄來說,其過程與插入屬於該索引塊索引範圍的鍵值的過程沒有區別。甚至你會發現,被插入的16的鍵值所處的位置與插入的13的鍵值所在的位置完全一樣(row#0[121]裏的121表示在索引塊中的位置)。也就是說,oracle並沒有重用原來爲16的鍵值,而是直接將所有標記爲D的索引條目清除出索引塊,然後插入新的鍵值爲16的索引條目。
對於第三種情況,我們已經可以根據前面有關第一、第二種情況做出預測,由於420塊已經被充滿,同時所插入的鍵值是整個表裏的最大值,因此也不會因此420號塊的分裂,而是直接獲取一個新的索引塊來存放該鍵值。但是419號塊裏標記爲D的索引條目是否能被清除出索引塊呢?
SQL> insert into index_test values (rpad('M',150,'M'));
SQL> alter system dump datafile 7 block 419;
SQL> alter system dump datafile 7 block 420;
SQL> alter system dump datafile 7 block 421;
打開跟蹤文件,可以清楚的看到,419號塊裏的標記爲D的4各索引條目仍然保留在索引塊裏,同時420號塊裏的內容沒有任何變化,而421號塊裏則存放了新的鍵值:rpad('M',150,'M')。
我們看看index_stats視圖會如何變化。其結果也符合我們從轉儲文件中所看到的內容。
SQL> analyze index idx_test validate structure;
SQL> select LF_ROWS,DEL_LF_ROWS,DEL_LF_ROWS_LEN,USED_SPACE,BTREE_SPACE from index_stats;
LF_ROWS DEL_LF_ROWS DEL_LF_ROWS_LEN USED_SPACE BTREE_SPACE
---------- ----------- --------------- ---------- -----------
21 4 652 3441 7456
既然當插入rpad('M',150,'M')時對419號塊沒有任何影響,不會將標記爲D的索引條目移出索引塊。那麼如果我們事先將419號索引塊中所有的索引條目都標記爲D,也就是說刪除419號索引塊中索引條目所對應的記錄,然後再次插入rpad('M',150,'M')時會發生什麼?通過測試,我們可以發現,再次插入一個最大值以後,該最大值會進入塊421裏,但是塊419裏的索引條目則會被全部清除,變成了一個空的索引數據塊。這也就是我們通常所說的,當索引塊裏的索引條目全部被設置爲D(刪除)標記時,再次插入任何一個索引鍵值都會引起該索引塊裏的內容被清除。
最後,我們來測試一下,當索引塊裏的索引條目全部被設置爲D(刪除)標記以後,再次插入新的鍵值時會如何重用這些索引塊。我們先創建一個測試表,並插入10000條記錄。
SQL> create table delete_test(id number);
SQL> begin
2 for i in 1..10000 loop
3 insert into delete_test values (i);
4 end loop;
5 commit;
6 end;
7 /
SQL> create index idx_delete_test on delete_test(id);
SQL> analyze index idx_delete_test validate structure;
SQL> select LF_ROWS,LF_BLKS,DEL_LF_ROWS,USED_SPACE,BTREE_SPACE from index_stats;
LF_ROWS LF_BLKS DEL_LF_ROWS USED_SPACE BTREE_SPACE
---------- ---------- ----------- ---------- -----------
10000 21 0 150021 176032
可以看到,該索引具有21個葉子節點。然後我們刪除前9990條記錄。從而使得21個葉子節點中只有最後一個葉子節點具有有效索引條目,前20個葉子節點裏的索引條目全都標記爲D(刪除)標記。
SQL> delete delete_test where id >= 1 and id <= 9990;
SQL> commit;
SQL> analyze index idx_delete_test validate structure;
SQL> select LF_ROWS,LF_BLKS,DEL_LF_ROWS,USED_SPACE,BTREE_SPACE from index_stats;
LF_ROWS LF_BLKS DEL_LF_ROWS USED_SPACE BTREE_SPACE
---------- ---------- ----------- ---------- -----------
10000 21 9990 150021 176032
最後,我們插入從20000開始到30000結束,共10000條與被刪除記錄完全不重疊的記錄。
SQL> begin
2 for i in 20000..30000 loop
3 insert into delete_test values (i);
4 end loop;
5 commit;
6 end;
7 /
SQL> analyze index idx_delete_test validate structure;
SQL> select LF_ROWS,LF_BLKS,DEL_LF_ROWS,USED_SPACE,BTREE_SPACE from index_stats;
LF_ROWS LF_BLKS DEL_LF_ROWS USED_SPACE BTREE_SPACE
---------- ---------- ----------- ---------- -----------
10011 21 0 160302 176032
很明顯的看到,儘管被插入的記錄不屬於被刪除的記錄範圍,但是隻要索引塊中所有的索引條目都被刪除了(標記爲D),該索引就變成可用索引塊而能夠被新的鍵值重新利用了。
因此,根據上面我們所做的試驗,可以對索引的刪除情況總結如下:
1) 當刪除表裏的一條記錄時,其對應於索引裏的索引條目並不會被物理的刪除,只是做了一個刪除標記。
2) 當一個新的索引條目進入一個索引葉子節點的時候,oracle會檢查該葉子節點裏是否存在被標記爲刪除的索引條目,如果存在,則會將所有具有刪除標記的索引條目從該葉子節點裏物理的刪除。
3) 當一個新的索引條目進入索引時,oracle會將當前所有被清空的葉子節點(該葉子節點中所有的索引條目都被設置爲刪除標記)收回,從而再次成爲可用索引塊。
儘管被刪除的索引條目所佔用的空間大部分情況下都能夠被重用,但仍然存在一些情況可能導致索引空間
被浪費,並造成索引數據塊很多但是索引條目很少的後果,這時該索引可以認爲出現碎片。而導致索引出現碎片的情況主要包括:
1) 不合理的、較高的PCTFREE。很明顯,這將導致索引塊的可用空間減少。
2) 索引鍵值持續增加(比如採用sequence生成序列號的鍵值),同時對索引鍵值按照順序連續刪除,這時可能導致索引碎片的發生。因爲前面我們知道,某個索引塊中刪除了部分的索引條目,只有當有鍵值進入該索引塊時才能將空間收回。而持續增加的索引鍵值永遠只會向插入排在前面的索引塊中,因此這種索引裏的空間幾乎不能收回,而只有其所含的索引條目全部刪除時,該索引塊才能被重新利用。
3) 經常被刪除或更新的鍵值,以後幾乎不再會被插入時,這種情況與上面的情況類似。
對於如何判斷索引是否出現碎片,方法非常簡單:直接運行ANALYZE INDEX … VALIDATE STRUCTURE
命令,然後檢查index_stats視圖的pct_used字段,如果該字段過低(低於50%),則說明存在碎片。
4.3 B樹索引的對於更新(UPDATE)的管理
而對於值被更新對於索引條目的影響,則可以認爲是刪除和插入的組合。也就是將被更新的舊值對應的索
引條目設置爲D(刪除)標記,同時將更新後的值按照順序插入合適的索引塊中。這裏就不重複討論了。
5. 重建B樹索引
5.1如何重建B樹索引
重建索引有兩種方法:一種是最簡單的,刪除原索引,然後重建;第二種是使用ALTER INDEX … REBUILD
命令對索引進行重建。第二種方式是從oracle 7.3.3版本開始引入的,從而使得用戶在重建索引時不必刪除原索引再重新CREATE INDEX了。ALTER INDEX … REBUILD相對CREATE INDEX有以下好處:
1) 它使用原索引的葉子節點作爲新索引的數據來源。我們知道,原索引的葉子節點的數據塊通常都要比表裏的數據塊要少很多,因此進行的I/O就會減少;同時,由於原索引的葉子節點裏的索引條目已經排序了,因此在重建索引的過程中,所做的排序工作也要少的多。
2) 自從oracle 8.1.6以來,ALTER INDEX … REBUILD命令可以添加ONLINE短語。這使得在重建索引的過程中,用戶可以繼續對原來的索引進行修改,也就是說可以繼續對錶進行DML操作。
而同時,ALTER INDEX … REBUILD與CREATE INDEX也有很多相同之處:
1) 它們都可以通過添加PARALLEL提示進行並行處理。
2) 它們都可以通過添加NOLOGGING短語,使得重建索引的過程中產生最少的重做條目(redo entry)。
3) 自從oracle 8.1.5以來,它們都可以田間COMPUTE STATISTICS短語,從而在重建索引的過程中,就生成CBO所需要的統計信息,這樣就避免了索引創建完畢以後再次運行analyze或dbms_stats來收集統計信息。
當我們重建索引以後,在物理上所能獲得的好處就是能夠減少索引所佔的空間大小(特別是能夠減少葉子
節點的數量)。而索引大小減小以後,又能帶來以下若干好處:
1) CBO對於索引的使用可能會產生一個較小的成本值,從而在執行計劃中選擇使用索引。
2) 使用索引掃描的查詢掃描的物理索引塊會減少,從而提高效率。
3) 由於需要緩存的索引塊減少了,從而讓出了內存以供其他組件使用。
儘管重建索引具有一定的好處,但是盲目的認爲重建索引能夠解決很多問題也是不正確的。比如我見過一
個生產系統,每隔一個月就要重建所有的索引(而且我相信,很多生產系統可能都會這麼做),其中包括一些100GB的大表。爲了完成重建所有的索引,往往需要把這些工作分散到多個晚上進行。事實上,這是一個7×24的系統,僅重建索引一項任務就消耗了非常多的系統資源。但是每隔一段時間就重建索引有意義嗎?這裏就有一些關於重建索引的很流行的說法,主要包括:
1) 如果索引的層級超過X(X通常是3)級以後需要通過重建索引來降低其級別。
2) 如果經常刪除索引鍵值,則需要定時重建索引來收回這些被刪除的空間。
3) 如果索引的clustering_factor很高,則需要重建索引來降低該值。
4) 定期重建索引能夠提高性能。
對於第一點來說,我們在前面已經知道,B樹索引是一棵在高度上平衡的樹,所以重建索引基本不可能降
低其級別,除非是極特殊的情況導致該索引有非常大量的碎片,導致B樹索引“虛高”,那麼這實際又來到第二點上(因爲碎片通常都是由於刪除引起的)。實際上,對於第一和第二點,我們應該通過運行ALTER INDEX … REBUILD命令以後檢查indest_stats.pct_used字段來判斷是否有必要重建索引。
5.2重建B樹索引對於clustering_factor的影響
而對於clustering_factor來說,它是用來比較索引的順序程度與表的雜亂排序程度的一個度量。Oracle在計算某個clustering_factor時,會對每個索引鍵值查找對應到表的數據,在查找的過程中,會跟蹤從一個表的數據塊跳轉到另外一個數據塊的次數(當然,它不可能真的這麼做,源代碼裏只是簡單的掃描索引,從而獲得ROWID,然後從這些ROWID獲得表的數據塊的地址)。每一次跳轉時,有個計數器就會增加,最終該計數器的值就是clustering_factor。下圖四描述了這個原理。
圖四
在上圖四中,我們有一個表,該表有4個數據塊,以及20條記錄。在列N1上有一個索引,上圖中的每個小黑點就表示一個索引條目。列N1的值如圖所示。而N1的索引的葉子節點包含的值爲:A、B、C、D、E、F。如果oracle開始掃描索引的底部,葉子節點包含的第一個N1值爲A,那麼根據該值可以知道對應的ROWID位於第一個數據塊的第三行裏,所以我們的計數器增加1。同時,A值還對應第二個數據塊的第四行,由於跳轉到了不同的數據塊上,所以計數器再加1。同樣的,在處理B時,可以知道對應第一個數據塊的第二行,由於我們從第二個數據塊跳轉到了第一個數據塊,所以計數器再加1。同時,B值還對應了第一個數據塊的第五行,由於我們這裏沒有發生跳轉,所以計數器不用加1。
在上面的圖裏,在表的每一行的下面都放了一個數字,它用來顯示計數器跳轉到該行時對應的值。當我們處理完索引的最後一個值時,我們在數據塊上一共跳轉了十次,所以該索引的clustering_factor爲10。
注意第二個數據塊,clustering_factor爲8出現了4次。因爲在索引裏N1爲E所對應的4個索引條目都指向了同一個數據塊。從而使得clustering_factor不再增長。同樣的現象出現在第三個數據塊中,它包含三條記錄,它們的值都是C,對應的clustering_factor都是6。
從clustering_factor的計算方法上可以看出,我們可以知道它的最小值就等於表所含有的數據塊的數量;而最大值就是表所含有的記錄的總行數。很明顯,clustering_factor越小越好,越小說明通過索引查找表裏的數據行時需要訪問的表的數據塊越少。
我們來看一個例子,來說明重建索引對於減小clustering_factor沒有用處。首先我們創建一個測試表:
SQL> create table clustfact_test(id number,name varchar2(10));
SQL> create index idx_clustfact_test on clustfact_test(id);
然後,我們插入十萬條記錄。
SQL> begin
2 for i in 1..100000 loop
3 insert into clustfact_test values(mod(i,200),to_char(i));
4 end loop;
5 commit;
6 end;
7 /
因爲使用了mod的關係,最終數據在表裏排列的形式爲:
0,1,2,3,4,5,…,197,198,199,0,1,2,3,…, 197,198,199,0,1,2,3,…, 197,198,199,0,1,2,3,…
接下來,我們分析表。
SQL> exec dbms_stats.gather_table_stats(user,'clustfact_test',cascade=>true);
這個時候,我們來看看該索引的clustering_factor。
SQL> select num_rows, blocks from user_tables where table_name = 'CLUSTFACT_TEST';
NUM_ROWS BLOCKS
---------- ----------
100000 202
SQL> select num_rows, distinct_keys, avg_leaf_blocks_per_key, avg_data_blocks_per_key,
2 clustering_factor from user_indexes where index_name = 'IDX_CLUSTFACT_TEST';
NUM_ROWS DISTINCT_KEYS AVG_LEAF_BLOCKS_PER_KEY AVG_DATA_BLOCKS_PER_KEY CLUSTERING_FACTOR
---------- ------------- ----------------------- ----------------------- -----------------
100000 200 1 198 39613
從上面的avg_data_blocks_per_key的值爲198可以知道,每個鍵值平均分佈在198個數據塊裏,而整個表也就202個數據塊。這也就是說,要獲取某個鍵值的所有記錄,幾乎每次都需要訪問所有的數據塊。從這裏已經可以猜測到clustering_factor會非常大。事實上,該值近4萬,也說明該索引並不會很有效。
我們來看看下面這句SQL語句的執行計劃。
SQL> select count(name) from clufac_test where id = 100;
Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT ptimizer=CHOOSE (Cost=32 Card=1 Bytes=9)
1 0 SORT (AGGREGATE)
2 1 TABLE ACCESS (FULL) OF 'CLUFAC_TEST' (Cost=32 Card=500 Bytes=4500)
Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
205 consistent gets
……
很明顯,CBO棄用了索引,而使用了全表掃描。這實際上已經說明由於索引的clustering_factor過高,導致通過索引獲取數據時跳轉的數據塊過多,成本過高,因此直接使用全表掃描的成本會更低。
這時我們來重建索引看看會對clustering_factor產生什麼影響。從下面的測試中可以看到,沒有任何影響。
SQL> alter index idx_clustfact_test rebuild;
SQL> select num_rows, distinct_keys, avg_leaf_blocks_per_key, avg_data_blocks_per_key,
2 clustering_factor from user_indexes where index_name = 'IDX_CLUSTFACT_TEST';
NUM_ROWS DISTINCT_KEYS AVG_LEAF_BLOCKS_PER_KEY AVG_DATA_BLOCKS_PER_KEY CLUSTERING_FACTOR
---------- ------------- ----------------------- ----------------------- -----------------
100000 200 1 198 39613
那麼當我們將表裏的數據按照id的順序(也就是索引的排列順序)重建時,該SQL語句會如何執行?
SQL> create table clustfact_test_temp as select * from clustfact_test order by id;
SQL> truncate table clustfact_test;
SQL> insert into clustfact_test select * from clustfact_test_temp;
SQL> exec dbms_stats.gather_table_stats(user,'clustfact_test',cascade=>true);
SQL> select num_rows, distinct_keys, avg_leaf_blocks_per_key, avg_data_blocks_per_key,
2 clustering_factor from user_indexes where index_name = 'IDX_CLUSTFACT_TEST';
NUM_ROWS DISTINCT_KEYS AVG_LEAF_BLOCKS_PER_KEY AVG_DATA_BLOCKS_PER_KEY CLUSTERING_FACTOR
---------- ------------- ----------------------- ----------------------- -----------------
100000 200 1 1 198
很明顯的,這時的索引裏每個鍵值只分布在1個數據塊裏,同時clustering_factor也已經降低到了198。這時再次執行相同的查詢語句時,CBO將會選擇索引,同時可以看到consistent gets也從205降到了5。
SQL> select count(name) from clustfact_test where id = 100;
Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT ptimizer=CHOOSE (Cost=2 Card=1 Bytes=9)
1 0 SORT (AGGREGATE)
2 1 TABLE ACCESS (BY INDEX ROWID) OF 'CLUSTFACT_TEST' (Cost=2 Card=500 Bytes=4500)
3 2 INDEX (RANGE SCAN) OF 'IDX_CLUSTFACT_TEST' (NON-UNIQUE) (Cost=1 Card=500)
Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
5 consistent gets
……
所以我們可以得出結論,如果僅僅是爲了降低索引的clustering_factor而重建索引沒有任何意義。降低clustering_factor的關鍵在於重建表裏的數據。只有將表裏的數據按照索引列排序以後,才能切實有效的降低clustering_factor。但是如果某個表存在多個索引的時候,需要仔細決定應該選擇哪一個索引列來重建表。
5.3重建B樹索引對於查詢性能的影響
最後我們來看一下重建索引對於性能的提高到底會有什麼作用。假設我們有一個表,該表具有1百萬條記錄,佔用了100000個數據塊。而在該表上存在一個索引,在重建之前的pct_used爲50%,高度爲3,分支節點塊數爲40個,再加一個根節點塊,葉子節點數爲10000個;重建該索引以後,pct_used爲90%,高度爲3,分支節點塊數下降到20個,再加一個根節點塊,而葉子節點數下降到5000個。那麼從理論上說:
1) 如果通過索引獲取單獨1條記錄來說:
重建之前的成本:1個根+1個分支+1個葉子+1個表塊=4個邏輯讀
重建之後的成本:1個根+1個分支+1個葉子+1個表塊=4個邏輯讀
性能提高百分比:0
2) 如果通過索引獲取100條記錄(佔總記錄數的0.01%)來說,分兩種情況:
最差的clustering_factor(即該值等於表的數據行數):
重建之前的成本:1個根+1個分支+0.0001*10000(1個葉子)+100個表塊=103個邏輯讀
重建之後的成本:1個根+1個分支+0.0001*5000(1個葉子)+100個表塊=102.5個邏輯讀
性能提高百分比:0.5%(也就是減少了0.5個邏輯讀)
最好clustering_factor(即該值等於表的數據塊):
重建之前的成本:1個根+1個分支+0.0001*10000(1個葉子)+0.0001*100000(10個表塊)=13個邏輯讀
重建之後的成本:1個根+1個分支+0.0001*5000(1個葉子)+0.0001*100000(10個表塊)=12.5個邏輯讀
性能提高百分比:3.8%(也就是減少了0.5個邏輯讀)
3) 如果通過索引獲取10000條記錄(佔總記錄數的1%)來說,分兩種情況:
最差的clustering_factor(即該值等於表的數據行數):
重建之前的成本:1個根+1個分支+0.01*10000(100個葉子)+10000個表塊=10102個邏輯讀
重建之後的成本:1個根+1個分支+0.01*5000(50個葉子)+10000個表塊=10052個邏輯讀
性能提高百分比:0.5%(也就是減少了50個邏輯讀)
最好clustering_factor(即該值等於表的數據塊):
重建之前的成本:1個根+1個分支+0.01*10000(100個葉子)+0.01*100000(1000個表塊)=1102個邏輯讀
重建之後的成本:1個根+1個分支+0.01*5000(50個葉子)+0.01*100000(1000個表塊)=1052個邏輯讀
性能提高百分比:4.5%(也就是減少了50個邏輯讀)
4) 如果通過索引獲取100000條記錄(佔總記錄數的10%)來說,分兩種情況:
最差的clustering_factor(即該值等於表的數據行數):
重建之前的成本:1個根+1個分支+0.1*10000(1000個葉子)+100000個表塊=101002個邏輯讀
重建之後的成本:1個根+1個分支+0.1*5000(500個葉子)+100000個表塊=100502個邏輯讀
性能提高百分比:0.5%(也就是減少了500個邏輯讀)
最好clustering_factor(即該值等於表的數據塊):
重建之前的成本:1個根+1個分支+0.1*10000(1000個葉子)+0.1*100000(10000個表塊)=11002個邏輯讀
重建之後的成本:1個根+1個分支+0.1*5000(500個葉子)+0.1*100000(10000個表塊)=10502個邏輯讀
性能提高百分比:4.5%(也就是減少了500個邏輯讀)
5) 對於快速全索引掃描來說,假設每次獲取8個數據塊:
重建之前的成本:(1個根+40個分支+10000個葉子)/ 8=1256個邏輯讀
重建之後的成本:(1個根+40個分支+5000個葉子)/ 8=631個邏輯讀
性能提高百分比:49.8%(也就是減少了625個邏輯讀)
從上面有關性能提高的理論描述可以看出,對於通過索引獲取的記錄行數不大的情況下,索引碎片對於性能的影響非常小;當通過索引獲取較大的記錄行數時,索引碎片的增加可能導致對於索引邏輯讀的增加,但是索引讀與表讀的比例保持不變;同時,我們從中可以看到,clustering_factor對於索引讀取的性能有很大的影響,並且對於索引碎片所帶來的影響具有很大的作用;最後,看起來,索引碎片似乎對於快速全索引掃描具有最大的影響。
我們來看兩個實際的例子,分別是clustering_factor爲最好和最差的兩個例子。測試環境爲8KB的數據塊,表空間採用ASSM的管理方式。先做一個最好的clustering_factor的例子,創建測試表並填充1百萬條數據。
SQL> create table rebuild_test(id number,name varchar2(10));
SQL> begin
2 for i in 1..1000000 loop
3 insert into rebuild_test values(i,to_char(i));
4 if mod(i,10000)=0 then
5 commit;
6 end if;
7 end loop;
8 end;
9 /
該表具有1百萬條記錄,分佈在2328個數據塊中。同時由於我們的數據都是按照順序遞增插入的,所以可以知道,在id列上創建的索引都是具有最好的clustering_factor值的。我們運行以下查詢測試語句,分別返回1、100、1000、10000、50000、100000以及1000000條記錄。
select * from rebuild_test where id = 10;
select * from rebuild_test where id between 100 and 199;
select * from rebuild_test where id between 1000 and 1999;
select * from rebuild_test where id between 10000 and 19999;
select /*+ index(rebuild_test) */ * from rebuild_test where id between 50000 and 99999;
select /*+ index(rebuild_test) */ * from rebuild_test where id between 100000 and 199999;
select /*+ index(rebuild_test) */ * from rebuild_test where id between 1 and 1000000;
select /*+ index_ffs(rebuild_test) */ id from rebuild_test where id between 1 and 1000000;
在運行這些測試語句前,先創建一個pctfree爲50%的索引,來模擬索引碎片,分析並記錄索引信息。
SQL> create index idx_rebuild_test on rebuild_test(id) pctfree 50;
SQL> exec dbms_stats.gather_table_stats(user,'rebuild_test',cascade=>true);
然後運行測試語句,記錄每條查詢語句所需的時間;接下來以pctfree爲10%重建索引,來模擬修復索引碎片,分析並記錄索引信息。
SQL> alter index idx_rebuild_test rebuild pctfree 10;
SQL> exec dbms_stats.gather_table_stats(user,'rebuild_test',cascade=>true);
接着再次運行這些測試語句,記錄每條查詢語句所需的時間。下表顯示了兩個索引信息的對比情況。
pctfree |
Height |
blocks |
br_blks |
lf_blks |
pct_used |
clustering_factor |
50% |
3 |
4224 |
8 |
4096 |
49% |
2326 |
10% |
3 |
2304 |
5 |
2226 |
90% |
2326 |
下表顯示了不同的索引下,運行測試語句所需的時間對比情況。
記錄數 |
佔記錄總數的百分比 |
pctused(50%) |
pctused(90%) |
性能提高百分比 |
1條記錄 |
0.0001% |
0.01 |
0.01 |
0.00% |
100條記錄 |
0.0100% |
0.01 |
0.01 |
0.00% |
1000條記錄 |
0.1000% |
0.01 |
0.01 |
0.00% |
10000條記錄 |
1.0000% |
0.02 |
0.02 |
0.00% |
50000條記錄 |
5.0000% |
0.06 |
0.06 |
0.00% |
100000條記錄 |
10.0000% |
1.01 |
1.00 |
0.99% |
1000000條記錄 |
100.0000% |
13.05 |
11.01 |
15.63% |
1000000條記錄(FFS) |
100.0000% |
7.05 |
7.02 |
0.43% |
上面是對最好的clustering_factor所做的測試,那麼對於最差的clustering_factor會怎麼樣呢?我們將rebuild_test中的id值反過來排列,也就是說,比如對於id爲3478的記錄,將id改爲8743。這樣的話,就將把原來按順序排列的id值徹底打亂,從而使得id上的索引的clustering_factor變成最差的。爲此,我寫了一個函數用來反轉id的值。
create or replace function get_reverse_value(id in number) return varchar2 is
ls_id varchar2(10);
ls_last_item varchar2(10);
ls_curr_item varchar2(10);
ls_zero varchar2(10);
li_len integer;
lb_stop boolean;
begin
ls_id := to_char(id);
li_len := length(ls_id);
ls_last_item := '';
ls_zero := '';
lb_stop := false;
while li_len>0 loop
ls_curr_item := substr(ls_id,li_len,1);
if ls_curr_item = '0' and lb_stop = false then
ls_zero := ls_zero || ls_curr_item;
else
lb_stop := true;
ls_last_item:=ls_last_item||ls_curr_item;
end if;
ls_id := substr(ls_id,1,li_len-1);
li_len := length(ls_id);
end loop;
return(ls_last_item||ls_zero);
end get_reverse_value;
接下來,我們創建我們第二個測試的測試表。並按照與第一個測試案例相同的方式進行測試。注意,對於測試查詢來說,要把表名(包括提示裏的)改爲rebuild_test_cf。
SQL> create table rebuild_test_cf as select * from rebuild_test;
SQL> update rebuild_test_cf set name=get_reverse_value(id);