1.什麼是B-Tree 和 B+Tree,他們是作什麼用的?數據庫
B-Tree是爲了磁盤或其它存儲設備而設計的一種多叉平衡查找樹,B-Tree 和 B+Tree 普遍應用於文件存儲系統以及數據庫系統中。數組
一、每一個節點最多擁有m個子樹數據結構
二、根節點至少有2個子樹性能
三、分支節點至少擁有m/2顆子樹(除根節點和葉子節點外都是分支節點)優化
四、全部葉子節點都在同一層spa
五、每一個節點最多能夠有m-1個key設計
六、每一個節點中的key以升序排列指針
七、節點中key元素左節點的全部值都小於或等於該元素,元素右節點的全部值都大於或等於該元素blog
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 操做的圖,咱們能看出來,元素就是相似 一、二、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 的結構很像,可是也有幾個本身的特性:
全部的非葉子節點只存儲關鍵字信息。
全部衛星數據(具體數據)都存在葉子結點中。
全部的葉子結點中包含了所有元素的信息。
全部葉子節點之間都有一個鏈指針。
若是上面 B-Tree 的圖變成 B+Tree,那應該以下:
細對比於 B-Tree 的圖,他們之間存在如下不一樣:
非葉子結點上已經只有 Key 信息了,知足上面第 1 點特性!
全部葉子結點下面都有一個 Data 區域,知足上面第 2 點特性!
非葉子結點的數據在葉子結點上都能找到,如根結點的元素 四、8 在最底層的葉子結點上也能找到,知足上面第 3 點特性!
注意圖中葉子結點之間的箭頭,知足上面第 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 依然做爲根結點,可是變成了一個不存放數據只存放索引的頁,而且有兩個子結點 Page二、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 個?