教你如何成為Oracle 10g OCP - 第九章 物件管理(6) - B樹索引的訪問

tolywang發表於2011-02-14

9.2.2  B樹索引的訪問


我們已經知道了B樹索引的體系結構,那麼當oracle需要訪問索引裡的某個索引條目時,
oracle是如何找到該索引條目所在的資料塊的呢?

當oracle程式需要訪問資料檔案裡的資料塊時,oracle會有兩種型別的I/O操作方式:

A. 順序訪問,每次讀取一個資料塊(等待事件“db file sequential read 檔案順序讀取”)。
B. 隨機訪問,每次讀取多個資料塊(等待事件“db file scattered  read 檔案分散讀取”)。

第一種方式則是訪問索引裡的資料塊,而第二種方式的I/O操作屬於全表掃描。

參考常見等待事件:
http://space.itpub.net/35489/viewspace-84652 

-------------------------------------------------------------------------------
備註:

db file scattered read (檔案分散讀取)
這種情況通常顯示與全表掃描相關的等待。當資料庫進行全表掃時,基於效能的考慮,數
據會分散(scattered)讀入Buffer Cache。如果這個等待事件比較顯著,可能說明對於某些
全表掃描的表,沒有建立索引或者沒有建立合適的索引,我們可能需要檢查這些資料表已
確定是否進行了正確的設定。 當這個等待事件比較顯著時,可以結合v$session_longops
動態效能檢視來進行診斷,該檢視中記錄了長時間(執行時間超過6秒的)執行的事物,
可能很多是全表掃描操作

db file sequential read (檔案順序讀取)
這一事件通常顯示與單個資料塊相關的讀取操作(如索引讀取)。如果這個等待事件比較
顯著,可能表示在多表連線中,表的連線順序存在問題,可能沒有正確的使用驅動表;
或者可能說明不加選擇地使用了索引。

在大多數情況下我們說,透過索引可以更為快速的獲取記錄,所以對於一個編碼規範、
調整良好的資料庫,這個等待很大是很正常的。但是在很多情況下,使用索引並不是最
佳的選擇,比如讀取較大表中大量的資料,全表掃描可能會明顯快於索引掃描,所以在
開發中我們就應該注意,對於這樣的查詢應該進行避免使用索引掃描。

-------------------------------------------------------------------------------

 


我們看到前面對B樹索引的體系結構的描述,可以知道其為一個樹狀的立體結構。其對
應到資料檔案裡的排列當然還是一個平面的形式,也就是像下面這樣。因此,當oracle
需要訪問某個索引塊的時候,勢必會在這個結構上跳躍的移動。

/根/分支/分支/葉子/…/葉子/分支/葉子/葉子/…/葉子/分支/葉子/葉子/…/葉子/分支/.....

當oracle需要獲得一個索引塊時,首先從根節點開始,根據所要查詢的鍵值,從而知道
其所在的下一層的分支節點,然後訪問下一層的分支節點,再次同樣根據鍵值訪問再下
一層的分支節點,如此這般,最終訪問到最底層的葉子節點。可以看出,其獲得物理
I/O塊時,是一個接著一個,按照順序,序列進行的。在獲得最終物理塊的過程中,我們
不能同時讀取多個塊,因為我們在沒有獲得當前塊的時候是不知道接下來應該訪問哪個
塊的。因此,在索引上訪問資料塊時,會對應到db file sequential read等待事件,其
根源在於我們是按照順序從一個索引塊跳到另一個索引塊,從而找到最終的索引塊的。

那麼對於全表掃描來說,則不存在訪問下一個塊之前需要先訪問上一個塊的情況。全表
掃描時,oracle知道要訪問所有的資料塊,因此唯一的問題就是儘可能高效的訪問這些
資料塊。因此,這時oracle可以採用同步的方式,分幾批,同時獲取多個資料塊。這幾
批的資料塊在物理上可能是分散在表裡的,因此其對應到db file scattered read等待
事件。

 


9.2.2.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樹索引始終都是平衡的。注意,這裡的平衡是指每個葉子節點與根節點
的距離都是相同的。同時,從索引的分裂機制可以看出,當插入的鍵值始終都是增大
的時候,索引總是向右擴充套件;而當插入的鍵值始終都是減小的時候,索引則總是向左
擴充套件。

 

B-Tree索引結構參考:

http://space.itpub.net/?uid-9842-action-viewspace-itemid-324586

http://www.cublog.cn/u3/112761/showart_2218897.html

http://space.itpub.net/?uid-9842-action-viewspace-itemid-324139

http://www.itpub.net/thread-300772-1-1.html


 

深入研究Oracle B樹索引系列 -by hanson

 

..... 待續......


 

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

相關文章