掃描下方二維碼或者微信搜索公衆號
菜鳥飛呀飛
,便可關注微信公衆號,閱讀更多Spring源碼分析
、Java併發編程
和Netty源碼系列
文章。html
接上篇博客《索引數據結構之 B-Tree 與 B+Tree(上篇)》,本文將簡單介紹 B+Tree 中插入、查找、刪除等相關操做,以及對比 B-Tree 與 B+Tree 的區別,最後會重點分析爲何 MySQL 會選擇 B+Tree 做爲索引的數據結構,而不是 B-Tree。web
B+Tree 的大分部性質與 B-Tree 的性質同樣,例如:對於一個 m 階的 B+Tree 而言,根結點的關鍵字數量爲 1 <= k <= m-1,非根結點的關鍵字數量爲 m/2 <= k <= m-1;每個結點上,全部的關鍵字都是有序排列的;每一個結點最多 m 個子結點。 但 B+Tree 有幾個十分重要的性質與 B-Tree 不同。編程
B+Tree 的插入操做與 B-Tree 的插入操做相似,也存在結點的分裂狀況,不一樣的是,非葉子結點的關鍵字會在葉子結點中冗餘一份。下面以一顆 5 階的 B+Tree 爲例,看看數據的插入流程。 一次向 B+Tree 中插入數據:50、30、40、25,前面的步驟與 B-Tree 徹底同樣。 數組
當繼續向其中插入數據 20 時,此時結點的關鍵字數量達到 5,超過了 m-1,所以須要將結點從中間分裂,因此從最中間的關鍵字 30 這兒分裂,將 30 提取到父結點當中,30 左邊的全部關鍵字組成一個新的結點——左孩子,30 右邊的全部關鍵字組成一個新的結點——右孩子,因爲 B+Tree 中非葉子結點中的關鍵字會冗餘一份在葉子結點中,因此 30 這個關鍵字以及這個關鍵字所對應的數據 value 也會存放進右孩子當中。以下圖所示。 緩存
B+Tree 中根據關鍵字查找數據的流程與 B-Tree 也幾乎同樣,不一樣的是,B+Tree 的全部數據都存放在葉子結點,所以 B+Tree 最後須要一直查找到葉子結點這一層,查到關鍵字對應的數據後才返回,而 B-Tree 中非葉子結點也能夠存放數據,所以 B-Tree 中查找數據時,可能遍歷到非葉子結點時就找到了數據,就能夠提早結束尋找過程。 如圖所示,從圖中的數據中查找關鍵字 22 的數據。 微信
首先從根結點開始出發,要查找的關鍵字 22 大於根結點中的第一個關鍵字 19,所以進入 19 的右子樹繼續查找,即進入到關鍵字 22 和 30 所在的這個結點。因爲要查找的數據 22 等於當前節點的第一個關鍵字(此時因爲當前節點不是葉子結點,因此不能中止查找),所以繼續進入到關鍵字 22 的右子樹查找,即進入到關鍵字 2二、2五、26 所在的結點,因爲當前結點是葉子結點,並在結點中找到了關鍵字 22,所以將數據返回。如上圖中,紅色線條即表示的是本次查找的路徑。數據結構
因爲 B+Tree 的全部數據都存在葉子結點,因此若是要刪除的數據存在於 B+Tree 中,那麼最終必定是在葉子結點中刪除數據,同時若是關鍵字還存在與非葉子結點中,那麼就還須要修改非葉子結點中的指針。在刪除過程當中也涉及到找兄弟結點借關鍵字以及合併結點的狀況,操做與 B-Tree 相似。 下面如下圖中的數據爲例。 併發
從 B+Tree 中刪除 19。找到 19 所處的葉子結點,而後將 19 刪除,而後葉子結點中只剩下關鍵字 20 了。因爲 19 這個關鍵字還處於非葉子結點中,所以將 19 從非葉子結點中刪除,而後用關鍵字 20 替換。如圖。 編輯器
因爲葉子結點中只剩下 20 這一個關鍵字了,小於 m-1,所以須要向兄弟結點借元素,右邊的兄弟結點的元素個數大於 m/2,所以能夠將 22 借出,而後修改父節點的指針。因此最終狀態以下。 源碼分析
至於刪除的其餘複雜情形,能夠參照上篇文章索引數據結構之 B-Tree 與 B+Tree(上篇)中 B-Tree 的刪除示例或者在可視化網站進行動態演示(https://www.cs.usfca.edu/~galles/visualization/Algorithms.html),會更加直觀。
前面關於 B-Tree 和 B+Tree 的知識,介紹了那麼多,那麼它們究竟有什麼用呢?關於 MySQL 中索引的數據結構,網上有不少說法,有的地方說是 B-Tree,有的地方說成 B+Tree,那麼到底是哪種數據結構呢?答案是 B+Tree。爲何不是 B-Tree 呢?
簡單點的答案就是,B-Tree 的非葉子結點會存放數據,而 B+Tree 的非葉子結點不存放數據,只存放關鍵字,這樣同一個結點,B+Tree 的結點能造成的叉數越多,那麼存儲必定數量的數據,B+Tree 的高度越小,那麼插入、查找、刪除的性能會越高。(B-Tree 和 B+Tree 的插入、查找、刪除的時間複雜度與樹的高度成正相關)。
MySQL 中 Innodb 存儲引擎存儲數據的最小單元是頁,一頁的大小默認是 16KB,能夠經過以下命令查看具體的值,這個值能夠被修改。
show variables like 'innodb_page_size';
複製代碼
而對於索引樹結構中,一個結點的大小一般是一個數據頁的整數倍,即 16KB 的倍數,而一個結點的大小一般也是 16KB。
如今咱們假設一個關鍵字(key)的大小爲 2KB,它對應的數據(value)是 2KB,因此一對 key-value 的大小就是 4KB,且假設樹的高度都爲 3。
對於 B-Tree 而言,全部結點中存的都是 key-value,因此 B-Tree 的第一層,也就是根結點層,最多能存放 16KB/4KB=4 對 key-value,也就是最多分出 5 個叉(5 顆子樹)。
所以第二層最多有 5 個結點,每一個結點依舊是最多能存儲 16KB/4KB=4 對 key-value,每一個結點也是分出 5 個叉,因此第二層最多能存儲 20 對 key-value。
那麼第三層就會有 25 個結點,每一個結點最多能存放 16KB/4KB=4 對 key-value,因此第三層最多能存儲 25*4 = 100 對 key-value。
所以該 B-Tree 的三層一共能存放 4+20+100 = 124 對 key-value。
再來看下 B+Tree,第一層和第二層都是非葉子結點,只存放 key,第三層是葉子結點,存放 key-value。
第一層最多能存放 16KB/2KB =8 對 key-value,最多能分出 9 個叉;
由於第一層最多能分出 9 個叉,因此第二層最多能夠有 9 個結點,每一個結點也是最多能存放 16KB/2KB =8 對 key-value,,每一個結點能分出 9 個叉,所以第二層最多能存儲 72 對 key-value,分出 81 個叉;
由於第二層最多能分出 9 乘以 9 個叉,因此第三層最多有 81 個結點,因爲第三層是葉子結點,存放 key 和 value,所以每一個結點能存放 16KB/4KB=4 對 key-value。因此第三層一共最多能存放 81*4=324 對 key-value。
B+Tree 的全部數據都存放在葉子結點,即第三層,所以該 B+Tree 一共能存放 324 對 key-value。(遠遠大於B-Tree的124對key-value)
對比發現,一樣高度的樹,B+Tree 能存放的數據遠遠多於 B-Tree(這裏舉例時,假設的 key 的大小爲 2KB,實際的 key 遠遠比這個小,若是 key 越小,這個差距會愈來愈大)。前面咱們假設的是樹的高度不變,那反過來假設咱們要存儲的數據量是必定的,好比 5GB 的數據,那麼最終 B-Tree 的樹高確定是遠遠高於 B+Tree。
樹的高度越高,會有什麼影響呢?熟悉樹這種數據結構的同窗確定知道,一般樹的插入、查找、刪除的時間複雜度與樹的高度成正相關,樹越高,時間複雜度越高,性能越差,這個時間複雜度是理論推導出來的,接下來咱們經過 MySQL 的數據讀取爲例,來解釋一下爲何數越高,性能越差。
假設 MySQL 的某張表的大小爲 5GB,對於主鍵索引來講,若是用 B-Tree 來存儲,那麼它最終的高度多是 20(這個 20 是假設的一個值),而若是使用 B+Tree 來存儲,它最終的高度多是 4(這個 4 也是假設的一個值)。(這裏的高度 4 和 20 都是咱們假設的一個值,不是精確值,但反應的是 B+Tree 和 B-Tree 在相同數據量時最終樹高相差較大的現象。)
MySQL 中存儲數據是以頁爲單位的,在讀數據時,也是以頁爲單位將數據從磁盤加載進內存當中,每從磁盤中讀取一個頁,就會發生一次磁盤 IO。也就是說,在查找數據時,每遍歷一個結點,就意味着讀取一個數據頁,也就是發生一次 IO。
對於 B-Tree 而言,若是要查找的數據在樹的越底層,就意味着要發生的 IO 次數越多,樹越高,查找時可能發生的磁盤 IO 次數越多,磁盤 IO 越多,意味着程序的耗時會越長,性能越差。
而對於 B+Tree 而言,它的樹高相對 B-Tree 而言會低不少,並且全部的數據都是存放在葉子結點當中的,因此查詢數據時,必定是遍歷到葉子結點層,時間複雜度相對穩定,並且發生的磁盤 IO 次數較少,因此總體來說,B+Tree 性能更優,所以 MySQL 的索引數據結構選擇的是 B+Tree,而不是 B-Tree。
一般一顆 B+Tree 的高度爲 3 或者 4,這樣一次索引的查找一般最多隻須要發生 3-4 次磁盤 IO,而 MySQL 中因爲 buffer pool 緩衝池的機制,第一層的結點所對應的數據頁一般存在於緩存中,第二層的數據頁也有很大可能也存於緩存中(MySQL 的預讀機制、LRU 緩存淘汰機制等都有可能致使),所以在遍歷時,B+Tree 的第一層、第二層極有可能不會發生磁盤 IO,而是基於內存查找,因此一次數據查找,可能就只會發生 1-2 次磁盤 IO,這個查詢效率獲得了大大提升。
另外,B+Tree 的葉子結點中,存放了相鄰葉子結點的指針,所以在進行範圍查找時,不須要屢次遍歷整棵樹,只須要找到一個葉子結點後,經過指針,就能找到其餘的葉子結點,這是十分高效的一種作法。
本文前半部分主要介紹了 B+Tree 的性質以及經過對比 B-Tree 的插入、查找、刪除操做的流程,簡單介紹了 B+Tree 的插入、查找、刪除操做的流程。最後從 B+Tree 和 B-Tree 數據結構的不一樣之處出發,詳細說明了爲何 MySQL 中索引的數據結構是 B+Tree,而不是 B-Tree。
咱們知道了 MySQL 的索引結構爲何沒有選擇 B-Tree,而是選擇了 B+Tree,而那麼多高效的數據結構,例如:數組、哈希表、紅黑樹等,爲何 MySQL 沒有選擇它們呢?下一篇博客分析。