1. 程式人生 > >B-Tree 和 B+Tree 結構及應用,InnoDB 引擎, MyISAM 引擎

B-Tree 和 B+Tree 結構及應用,InnoDB 引擎, MyISAM 引擎

1.什麼是B-Tree 和 B+Tree,他們是做什麼用的?

    B-Tree是為了磁碟或其它儲存裝置而設計的一種多叉平衡查詢樹,B-Tree 和 B+Tree 廣泛應用於檔案儲存系統以及資料庫系統中。

    在大規模資料儲存中,實現索引查詢這樣一個實際背景下,樹節點儲存的元素數量是有限的(如果元素數量非常多的話,樹的高度就會增大,查詢就退化成節點內部的線性查找了),這樣導致二叉查詢樹結構由於樹的深度過大而造成磁碟I/O讀寫過於頻繁,進而導致查詢效率低下(為什麼會出現這種情況?這跟外部儲存器-磁碟的儲存方式有關)。那麼該如何減少樹的高度呢?一個基本的想法就是:採用多叉樹結構(每個節點存放多個元素,每個節點有多個子節點,這樣樹的高度就降低了)。根據平衡二叉樹的啟發,自然就想到平衡多路查詢樹結構。B-Tree的各種操作能使B-Tree保持較低的高度,從而達到有效避免磁碟過於頻繁的查詢存取操作,從而有效提高查詢效率。 2.B-Tree 2.1定義  m階B-Tree滿足以下條件:

1、每個節點最多擁有m個子樹

2、根節點至少有2個子樹

3、分支節點至少擁有m/2顆子樹(除根節點和葉子節點外都是分支節點)

4、所有葉子節點都在同一層

5、每個節點最多可以有m-1個key

6、每個節點中的key以升序排列

7、節點中key元素左節點的所有值都小於或等於該元素,元素右節點的所有值都大於或等於該元素

 下面是一個3階的B樹:

                                                                              

2.2.B-Tree的建立過程

下面我們以一個[0,1,2,3,4,5,6,7]的陣列插入一棵 3 階的 B-Tree 為例,將所有的條件都串起來!

那麼,你是否對 B-Tree 的幾點特性都清晰了呢?在二叉樹中,每個結點只有一個元素。

但是在 B-Tree 中,每個結點都可能包含多個元素,並且非葉子結點在元素的左右都有指向子結點的指標。

 

 

 

 在 B-Tree 中,每個結點都可能包含多個元素,並且非葉子結點在元素的左右都有指向子結點的指標。 

2.3.B-Tree搜尋原理

 如果需要查詢一個元素,那流程是怎麼樣的呢?我們看下圖,如果我們要在下面的 B-Tree 中找到關鍵字 24,那流程如下:

 

 

 

 

 

 

從這個流程我們能看出,B-Tree 的查詢效率好像也並不比平衡二叉樹高。但是查詢所經過的結點數量要少很多,也就意味著要少很多次的磁碟 IO,這對效能的提升是很大的。 

從前面對 B-Tree 操作的圖,我們能看出來,元素就是類似 1、2、3 這樣的數值。 

2.4.B-Tree在資料庫中的應用

但是資料庫的資料都是一條條的資料,如果某個資料庫以 B-Tree 的資料結構儲存資料,那資料怎麼存放的呢?

 

 普通的 B-Tree 的結點中,元素就是一個個的數字。但是上圖中,我們把元素部分拆分成了 key-data 的形式,Key 就是資料的主鍵,Data 就是具體的資料。 

這樣我們在找一條數的時候,就沿著根結點往下找就 OK 了,效率是比較高的。

 3.B+Tree

B+Tree 是在 B-Tree 基礎上的一種優化,使其更適合實現外儲存索引結構。 

B+Tree 與 B-Tree 的結構很像,但是也有幾個自己的特性:

    1. 所有的非葉子節點只儲存關鍵字資訊。 

    2. 所有衛星資料(具體資料)都存在葉子結點中。 

    3. 所有的葉子結點中包含了全部元素的資訊。 

    4. 所有葉子節點之間都有一個鏈指標。 

如果上面 B-Tree 的圖變成 B+Tree,那應該如下: 

 細對比於 B-Tree 的圖,他們之間存在以下不同:

    • 非葉子結點上已經只有 Key 資訊了,滿足上面第 1 點特性! 

    • 所有葉子結點下面都有一個 Data 區域,滿足上面第 2 點特性! 

    • 非葉子結點的資料在葉子結點上都能找到,如根結點的元素 4、8 在最底層的葉子結點上也能找到,滿足上面第 3 點特性! 

    • 注意圖中葉子結點之間的箭頭,滿足上面第 4 點特性!

4、B-Tree 和 B+Tree 該如何選擇呢?都有哪些優劣呢? 

①B-Tree 因為非葉子結點也儲存具體資料,所以在查詢某個關鍵字的時候找到即可返回。 

而 B+Tree 所有的資料都在葉子結點,每次查詢都得到葉子結點。所以在同樣高度的 B-Tree 和 B+Tree 中,B-Tree 查詢某個關鍵字的效率更高。  

②由於 B+Tree 所有的資料都在葉子結點,並且結點之間有指標連線,在找大於某個關鍵字或者小於某個關鍵字的資料的時候,B+Tree 只需要找到該關鍵字然後沿著連結串列遍歷就可以了,而 B-Tree 還需要遍歷該關鍵字結點的根結點去搜索。  

③由於 B-Tree 的每個結點(這裡的結點可以理解為一個數據頁)都儲存主鍵+實際資料,而 B+Tree 非葉子結點只儲存關鍵字資訊,而每個頁的大小是有限的,所以同一頁能儲存的 B-Tree 的資料會比 B+Tree 儲存的更少。 

這樣同樣總量的資料,B-Tree 的深度會更大,增大查詢時的磁碟 I/O 次數,進而影響查詢效率。  

鑑於以上的比較,所以在常用的關係型資料庫中,都是選擇 B+Tree 的資料結構來儲存資料! 

下面我們以 MySQL 的 InnoDB 儲存引擎為例講解,其他類似 SQL Server、Oracle 的原理! 

InnoDB 引擎資料儲存 

在 InnoDB 儲存引擎中,也有頁的概念,預設每個頁的大小為 16K,也就是每次讀取資料時都是讀取 4*4K 的大小! 

假設我們現在有一個使用者表,我們往裡面寫資料:

 

 這裡需要注意的一點是,在某個頁內插入新行時,為了減少資料的移動,通常是插入到當前行的後面或者是已刪除行留下來的空間,所以在某一個頁內的資料並不是完全有序的(後面頁結構部分有細講)。 

但是為了資料訪問順序性,在每個記錄中都有一個指向下一條記錄的指標,以此構成了一條單向有序連結串列,不過在這裡為了方便演示我是按順序排列的! 

由於資料還比較少,一個頁就能容下,所以只有一個根結點,主鍵和資料也都是儲存在根結點(左邊的數字代表主鍵,右邊名字、性別代表具體的資料)。 

假設我們寫入 10 條資料之後,Page1 滿了,再寫入新的資料會怎麼存放呢? 

我們繼續看下圖:

 

 有個叫“秦壽生”的朋友來了,但是 Page1 已經放不下資料了,這時候就需要進行頁分裂,產生一個新的 Page。 

在 InnoDB 中的流程是怎麼樣的呢?

    • 產生新的 Page2,然後將 Page1 的內容複製到 Page2。 

    • 產生新的 Page3,“秦壽生”的資料放入 Page3。 

    • 原來的 Page1 依然作為根結點,但是變成了一個不存放資料只存放索引的頁,並且有兩個子結點 Page2、Page3。 

這裡有兩個問題需要注意的是: 

①為什麼要複製 Page1 為 Page2 而不是建立一個新的頁作為根結點,這樣就少了一步複製的開銷了? 

如果是重新建立根結點,那根結點儲存的實體地址可能經常會變,不利於查詢。 

並且在 InnoDB 中根結點是會預讀到記憶體中的,所以結點的實體地址固定會比較好! 

②原來 Page1 有 10 條資料,在插入第 11 條資料的時候進行裂變,根據前面對 B-Tree、B+Tree 特性的瞭解,那這至少是一棵 11 階的樹,裂變之後每個結點的元素至少為 11/2=5 個。 

那是不是應該頁裂變之後主鍵 1-5 的資料還是在原來的頁,主鍵 6-11 的資料會放到新的頁,根結點存放主鍵 6?  

如果是這樣的話,新的頁空間利用率只有 50%,並且會導致更為頻繁的頁分裂。 

所以 InnoDB 對這一點做了優化,新的資料放入新建立的頁,不移動原有頁面的任何記錄。 

隨著資料的不斷寫入,這棵樹也逐漸枝繁葉茂,如下圖:

 

 每次新增資料,都是將一個頁寫滿,然後新建立一個頁繼續寫,這裡其實是有個隱含條件的,那就是主鍵自增! 

主鍵自增寫入時新插入的資料不會影響到原有頁,插入效率高!且頁的利用率高! 

但是如果主鍵是無序的或者隨機的,那每次的插入可能會導致原有頁頻繁的分裂,影響插入效率!降低頁的利用率!這也是為什麼在 InnoDB 中建議設定主鍵自增的原因! 

這棵樹的非葉子結點上存的都是主鍵,那如果一個表沒有主鍵會怎麼樣?在 InnoDB 中,如果一個表沒有主鍵,那預設會找建了唯一索引的列,如果也沒有,則會生成一個隱形的欄位作為主鍵! 

有資料插入那就有刪除,如果這個使用者表頻繁的插入和刪除,那會導致資料頁產生碎片,頁的空間利用率低,還會導致樹變的“虛高”,降低查詢效率!這可以通過索引重建來消除碎片提高查詢效率! 

InnoDB 引擎資料查詢 

資料插入了怎麼查詢呢?

    • 找到資料所在的頁。這個查詢過程就跟前面說到的 B+Tree 的搜尋過程是一樣的,從根結點開始查詢一直到葉子結點。 

    • 在頁內找具體的資料。讀取第 1 步找到的葉子結點資料到記憶體中,然後通過分塊查詢的方法找到具體的資料。 

這跟我們在新華字典中找某個漢字是一樣的,先通過字典的索引定位到該漢字拼音所在的頁,然後到指定的頁找到具體的漢字。 

InnoDB 中定位到頁後用了哪種策略快速查詢某個主鍵呢?這我們就需要從頁結構開始瞭解。

左邊藍色區域稱為 Page Directory,這塊區域由多個 Slot 組成,是一個稀疏索引結構,即一個槽中可能屬於多個記錄,最少屬於 4 條記錄,最多屬於 8 條記錄。 

槽內的資料是有序存放的,所以當我們尋找一條資料的時候可以先在槽中通過二分法查詢到一個大致的位置。 

右邊區域為資料區域,每一個數據頁中都包含多條行資料。注意看圖中最上面和最下面的兩條特殊的行記錄 Infimum 和 Supremum,這是兩個虛擬的行記錄。 

在沒有其他使用者資料的時候 Infimum 的下一條記錄的指標指向 Supremum。 

當有使用者資料的時候,Infimum 的下一條記錄的指標指向當前頁中最小的使用者記錄,當前頁中最大的使用者記錄的下一條記錄的指標指向 Supremum,至此整個頁內的所有行記錄形成一個單向連結串列。 

行記錄被 Page Directory 邏輯的分成了多個塊,塊與塊之間是有序的,也就是說“4”這個槽指向的資料塊內最大的行記錄的主鍵都要比“8”這個槽指向的資料塊內最小的行記錄的主鍵要小。但是塊內部的行記錄不一定有序。 

每個行記錄的都有一個 n_owned 的區域(圖中粉紅色區域),n_owned 標識這個塊有多少條資料。 

偽記錄 Infimum 的 n_owned 值總是 1,記錄 Supremum 的 n_owned 的取值範圍為[1,8],其他使用者記錄 n_owned 的取值範圍[4,8]。 

並且只有每個塊中最大的那條記錄的 n_owned 才會有值,其他的使用者記錄的 n_owned 為 0。 

所以當我們要找主鍵為 6 的記錄時,先通過二分法在稀疏索引中找到對應的槽,也就是 Page Directory 中“8”這個槽。 

“8”這個槽指向的是該資料塊中最大的記錄,而資料是單向連結串列結構,所以無法逆向查詢。 

所以需要找到上一個槽即“4”這個槽,然後通過“4”這個槽中最大的使用者記錄的指標沿著連結串列順序查詢到目標記錄。 

聚集索引&非聚集索引 

前面關於資料儲存的都是演示的聚集索引的實現,如果上面的使用者表需要以“使用者名稱字”建立一個非聚集索引,是怎麼實現的呢? 

我們看下圖:

 非聚集索引的儲存結構與前面是一樣的,不同的是在葉子結點的資料部分存的不再是具體的資料,而是資料的聚集索引的 Key。 

所以通過非聚集索引查詢的過程是先找到該索引 Key 對應的聚集索引的 Key,然後再拿聚集索引的 Key 到主鍵索引樹上查詢對應的資料,這個過程稱為回表!

InnoDB 與 MyISAM 引擎對比 

上面包括儲存和搜尋都是拿的 InnoDB 引擎為例,那 MyISAM 與 InnoDB 在儲存上有啥不同呢?看圖:

上圖為 MyISAM 主鍵索引的儲存結構,我們能看到的不同是:

    • 主鍵索引樹的葉子結點的資料區域沒有存放實際的資料,存放的是資料記錄的地址。 

    • 資料的儲存不是按主鍵順序存放的,是按寫入的順序存放。 

也就是說 InnoDB 引擎資料在物理上是按主鍵順序存放,而 MyISAM 引擎資料在物理上按插入的順序存放。 

並且 MyISAM 的葉子結點不存放資料,所以非聚集索引的儲存結構與聚集索引類似,在使用非聚集索引查詢資料的時候通過非聚集索引樹就能直接找到資料的地址了,不需要回表,這比 InnoDB 的搜尋效率會更高呢!

索引優化建議 

大家經常會在很多的文章或書中能看到一些索引的使用建議,比如說:

    • like 的模糊查詢以 % 開頭,會導致索引失效。 

    • 一個表建的索引儘量不要超過 5 個。 

    • 儘量使用覆蓋索引。 

    • 儘量不要在重複資料多的列上建索引。 

    • ...... 

很多這裡就不一一列舉了!那看完這篇文章,我們能否帶著疑問去分析一下為什麼要有這些建議? 

為什麼 like 的模糊查詢以 % 開頭,會導致索引失效?為什麼一個表建的索引儘量不要超過 5 個?