重溫算法Day23:B+樹

樹中的節點並不存儲數據本身,而是隻是作爲索引。除此之外,我們把每個葉子節點串在一條鏈表上,鏈表中的數據是從小到大有序的。 

如果我們要求某個區間的數據。我們只需要拿區間的起始值,在樹中進行查找,當查找到某個葉子節點之後,我們再順着鏈表往後遍歷,直到鏈表中的結點數據值大於區間的終止值爲止。所有遍歷到的數據,就是符合區間值的所有數據。

當數據量很大的時候,如果將索引存儲在內存中,儘管內存訪問的速度非常快,查詢的效率非常高,但是,佔用的內存會非常多

如果把索引存儲在硬盤中,而非內存中。但硬盤是一個非常慢速的存儲設備。通常內存的訪問速度是納秒級別的,而磁盤訪問的速度是毫秒級別的。讀取同樣大小的數據,從磁盤中讀取花費的時間,是從內存中讀取所花費時間的上萬倍,甚至幾十萬倍。

 

爲了節省內存,如果把樹存儲在硬盤中,那麼每個節點的讀取(或者訪問),都對應一次磁盤 IO 操作。樹的高度就等於每次查詢數據時磁盤 IO 操作的次數。比起內存讀寫操作,磁盤 IO 操作非常耗時,所以我們優化的重點就是儘量減少磁盤 IO 操作,也就是,儘量降低樹的高度。

如果我們將 m 叉樹實現 B+ 樹索引,用代碼實現出來。如下:

/**
 * 這是B+樹非葉子節點的定義。
 *
 * 假設keywords=[3, 5, 8, 10]
 * 4個鍵值將數據分爲5個區間:(-INF,3), [3,5), [5,8), [8,10), [10,INF)
 * 5個區間分別對應:children[0]...children[4]
 *
 * m值是事先計算得到的,計算的依據是讓所有信息的大小正好等於頁的大小:
 * PAGE_SIZE = (m-1)*4[keywordss大小]+m*8[children大小]
 */
type struct BPlusTreeNode {
   m       int             // 5叉樹
  keywords []int           // 鍵值,用來劃分數據區間
  children []BPlusTreeNode //保存子節點指針
}

/**
 * 這是B+樹中葉子節點的定義。
 *
 * B+樹中的葉子節點跟內部節點是不一樣的,
 * 葉子節點存儲的是值,而非區間。
 * 這個定義裏,每個葉子節點存儲3個數據行的鍵值及地址信息。
 *
 * k值是事先計算得到的,計算的依據是讓所有信息的大小正好等於頁的大小:
 * PAGE_SIZE = k*4[keyw..大小]+k*8[dataAd..大小]+8[prev大小]+8[next大小]
 */
type struct BPlusTreeLeafNode {
  k            int
  keywords     []int     // 數據的鍵值
  dataAddress  []long    // 數據地址

  prev BPlusTreeLeafNode // 這個結點在鏈表中的前驅結點
  next BPlusTreeLeafNode // 這個結點在鏈表中的後繼結點
}

對於相同個數的數據構建 m 叉樹索引,m 叉樹中的 m 越大,那樹的高度就越小,那 m 叉樹中的 m 是不是越大越好呢?到底多大才最合適呢?


不管是內存中的數據,還是磁盤中的數據,操作系統都是按頁(一頁大小通常是 4KB,這個值可以通過 getconfig PAGE_SIZE 命令查看)來讀取的,一次會讀一頁的數據。如果要讀取的數據量超過一頁的大小,就會觸發多次 IO 操作。所以,我們在選擇 m 大小的時候,要儘量讓每個節點的大小等於一個頁的大小。讀取一個節點,只需要一次磁盤 IO 操作。

數據的寫入過程,會涉及索引的更新,這是索引導致寫入變慢的主要原因。


對於一個 B+ 樹來說,m 值是根據頁的大小事先計算好的,也就是說,每個節點最多只能有 m 個子節點。在往數據庫中寫入數據的過程中,這樣就有可能使索引中某些節點的子節點個數超過 m,這個節點的大小超過了一個頁的大小,讀取這樣一個節點,就會導致多次磁盤 IO 操作。

正是因爲要時刻保證 B+ 樹索引是一個 m 叉樹,所以,索引的存在會導致數據庫寫入的速度降低。實際上,不光寫入數據會變慢,刪除數據也會變慢。

 

B- 樹就是 B 樹,B+ 樹中的節點不存儲數據,只是索引,而 B 樹中的節點存儲數據;B 樹中的葉子節點並不需要鏈表來串聯。B 樹只是一個每個節點的子節點個數不能小於 m/2 的 m 叉樹

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章