紅黑樹、B樹、B+樹各自適用的場景

1. 磁盤基礎(chǔ)知識

  • 分頁:

現(xiàn)代操作系統(tǒng)都使用虛擬內(nèi)存來印射到物理內(nèi)存,內(nèi)存大小有限且價格昂貴,所以數(shù)據(jù)的持久化是在磁盤上。虛擬內(nèi)存、物理內(nèi)存、磁盤都使用頁作為內(nèi)存讀取的最小單位。一般一頁為4KB(8個扇區(qū),每個扇區(qū)512B,8*512B=4KB)。

  • 局部性原理:
  1. 當(dāng)一個數(shù)據(jù)被用到時,其附近的數(shù)據(jù)也通常會馬上被使用。
  2. 程序運(yùn)行期間所需要的數(shù)據(jù)通常比較集中。
  • 磁盤預(yù)讀原理:

磁盤讀取依靠的是機(jī)械運(yùn)動,分為尋道時間、旋轉(zhuǎn)延遲、傳輸時間三個部分,這三個部分耗時相加就是一次磁盤IO的時間,大概 9ms 左右。這個成本是訪問內(nèi)存的十萬倍左右;

磁盤讀取的速度遠(yuǎn)小于內(nèi)存,所以盡量減少 I/O 次數(shù)是提高效率的關(guān)鍵。

根據(jù)局部性原理,且由于磁盤順序讀取的效率很高(不需要尋道時間,只需很少的旋轉(zhuǎn)時間),所以即使只需要讀取一個字節(jié),磁盤也會讀取一頁的數(shù)據(jù)。即磁盤預(yù)讀時通常會讀取頁的整倍數(shù)。

2. 樹基礎(chǔ)知識回顧

排序二叉樹:左 < 跟 < 右
B 樹:有序數(shù)組 + 多叉平衡樹,節(jié)點存儲關(guān)鍵字、數(shù)據(jù)、指針;
B+ 樹:有序數(shù)組鏈表 + 多叉平衡樹,非葉子節(jié)點存儲指針、關(guān)鍵字,不存儲數(shù)據(jù);
紅黑樹:紅黑樹是一種不大嚴(yán)格的平衡樹(平衡樹要求太高)

平衡樹是為了防止二叉查找樹退化為鏈表,而紅黑樹在維持平衡以確保 O(log2(n)) 的同時,不需要頻繁著調(diào)整樹的結(jié)構(gòu);

二叉樹的存儲結(jié)構(gòu)

  1. 順序存儲(適用于完全二叉樹)
二叉樹順序存儲

index 之間的對應(yīng)關(guān)系:

完全二叉樹順序存儲

注意:二叉樹的順序存儲只適合存儲完全二叉樹,否則 index 無法和節(jié)點對應(yīng)起來,會有點惡心:

非完全二叉樹順序存儲
  1. 鏈?zhǔn)酱鎯?/li>
二叉樹鏈?zhǔn)酱鎯?/div>

這里要好好理解一下,不然會影響后面的理解。

3. 為什么不能使用二叉樹來存儲數(shù)據(jù)庫索引

先說結(jié)論:

  1. 平衡二叉樹進(jìn)行插入/刪除時,大概率需要通過左旋/右旋來維持平衡;
  2. 旋轉(zhuǎn)需要加載整個樹,頻繁旋轉(zhuǎn)效率低;
  3. 二叉樹的 I/O 次數(shù)近似為 O(log2(n));
  4. 范圍查詢時,二叉樹的時間復(fù)雜度會退化成 O(n);
  5. 二叉樹退化成鏈表時,時間復(fù)雜度也近似退化成了 O(n);
  6. 二叉樹無法使用磁盤預(yù)讀功能;

其實單論范圍查詢,在關(guān)系型數(shù)據(jù)庫中就基本沒有使用二叉樹的可能了。但是為了加深對知識的了解,來看看其他的原因。

先剔除掉范圍查詢的情況,原因 1、2、6 可以通過紅黑樹來解決,那么其實就剩下 2 個原因:

  1. I/O 次數(shù)對比;
  2. 磁盤預(yù)讀功能的利用;

4. 二叉樹的 I/O 次數(shù)分析

先說 I/O 次數(shù):

其實相比于二叉樹,B 樹、B+樹, CPU 的運(yùn)算次數(shù)并沒有變化,甚至增多。但是 CPU 運(yùn)算次數(shù)相比于 I/O 的消耗而言,可以忽略不計,所以 I/O 次數(shù)是評價一個數(shù)據(jù)庫索引的效率高低的關(guān)鍵指標(biāo)。

對于紅黑樹而言,其 I/O 次數(shù)近似為 log2(n),為什么是近似呢?

首先,索引是存儲在磁盤上的,磁盤上的數(shù)據(jù)大部分情況下是連續(xù)的,但是隨著增刪改查的發(fā)生,有可能產(chǎn)生很多碎片,也就是說:

  • 索引在磁盤上的存儲也不一定是連續(xù)的;

這里,嚴(yán)謹(jǐn)起見,我們來分兩種情況:

  1. 索引節(jié)點,即樹的節(jié)點在磁盤上存儲是連續(xù);

假設(shè)一個頁能存儲 5 個節(jié)點,假設(shè)二叉樹如下:

二叉樹

注意,序號只代表在磁盤中存儲的順序,不代表對應(yīng)節(jié)點的關(guān)鍵字的值;

二叉樹可能是鏈?zhǔn)酱鎯?,也可能是順序存儲。但是這里假設(shè)節(jié)點在磁盤上的存儲是連續(xù)的,所以這里可以近似理解成順序存儲。即使是鏈?zhǔn)酱鎯Γ瑹o非就是 pNext 指針指向下一個連續(xù)的內(nèi)存地址而已。

現(xiàn)在假設(shè)搜索的結(jié)果是最左邊的葉子節(jié)點 16,因為磁盤預(yù)讀的特性,加上一個頁能存儲 5 個節(jié)點,第一次 I/O :

第一次I/O

如上,第一次 I/O 就讀取了 5 個節(jié)點,不僅把根節(jié)點讀取進(jìn)內(nèi)存了,還把節(jié)點 2 和 4 都讀取進(jìn)去了,看上去還節(jié)約了兩次 I/O ?好厲害的樣子......

此時,會根據(jù)二分法查找,對比 1 號節(jié)點然后去找節(jié)點 2,緊接著找節(jié)點 4,因為這兩個節(jié)點都在內(nèi)存中了,所以不需要進(jìn)行 I/O

這里再說一次,序號不代表節(jié)點的關(guān)鍵字,而是單純的表示節(jié)點在磁盤中的排列順序;

緊接著,會需要 8 號節(jié)點,而 8 不再內(nèi)存中,所以進(jìn)行第二次 I/O 同樣是讀取一頁,即 5 個節(jié)點:

第二次I/O

這次雖然也是讀取了 5 個節(jié)點,但是實際上只有 8 號節(jié)點有實際作用,其他節(jié)點并沒什么卵用(這是二叉樹無法使用預(yù)讀功能的本質(zhì)),但是現(xiàn)在還沒體現(xiàn)出劣勢,現(xiàn)在對比之后需要 16 號節(jié)點,繼續(xù)第三次讀?。?/p>

第三次I/O

此時找到了 16,并將結(jié)果返回。

這是高度為 4 的情況,且只有 31 個數(shù)據(jù)。但是實際使用中,怎么可能就 31 個數(shù)據(jù)?假設(shè)要找的是 32 號節(jié)點,因為 16 號節(jié)點之后的 17-20 雖然被加載進(jìn)內(nèi)存了,但是完全沒用。那么就需要再進(jìn)行一次 I/O 來加載 32 號節(jié)點所在的頁,同時也會將 33-36 加載進(jìn)內(nèi)存,但是這些節(jié)點并無卵用。

如果要找的是 1000 ,10000?

所以,隨著層級的深入,會出現(xiàn):

  1. 一個頁中只有一個節(jié)點有用(二分法查找要的是子節(jié)點而不是兄弟節(jié)點);
  2. I/O 次數(shù)近似等于log2(n);

即:

  1. 第一次 I/O 可能的優(yōu)勢在層級加深之后就沒有了;
  2. 就算是紅黑樹,也只能將時間復(fù)雜度維持在 log2(n);

上述討論的是索引樹在磁盤上的存儲是連續(xù)的,如果不是連續(xù)的,那么按頁讀取到的臟數(shù)據(jù)會更多,上述的情況中,前幾次 I/O 讀取到有用的數(shù)據(jù)的概率會變低,所以 I/O 的次數(shù)只會增多而不會減少,即仍然是近似于 log2(n)。

5. B/B+樹

B 樹即:多路平衡查找樹;

B 樹的巧妙之處在于:

  1. 將一個節(jié)點的大小設(shè)置為一頁的大小;
  2. 一個節(jié)點可以存放多個關(guān)鍵字(多叉樹);
  3. 自平衡;

這 3 點結(jié)合起來就可以做到:

  1. 一個節(jié)點大小為一頁,被加載進(jìn)內(nèi)存時,這些關(guān)鍵字在進(jìn)行對比,找出需要 leftChild 還是 rightChild 時,都是有用的(如最右側(cè)時需要對比所有節(jié)點);
  2. 一個節(jié)點可以存儲多個關(guān)鍵字,有效降低了樹的高度;

B+ 樹的巧妙之處在于:

  1. 非葉子節(jié)點不存儲數(shù)據(jù),進(jìn)一步增大了一頁中存儲關(guān)鍵字的數(shù)量;
  2. 葉子節(jié)點中存儲數(shù)據(jù)且存在指向下一頁的鏈表指針,可以使用順序查詢(支持范圍查詢);

6. B/B+樹的索引數(shù)量

B 樹的節(jié)點中存儲:指針、關(guān)鍵字(主鍵)、數(shù)據(jù)
B+ 樹的非葉子節(jié)點:指針、關(guān)鍵字
B+樹的葉子節(jié)點:指針(鏈表)、關(guān)鍵字、數(shù)據(jù)

注意,這里不是絕對的,比如有的 B+ 樹中葉子節(jié)點存儲的不是數(shù)據(jù),而是指向數(shù)據(jù)的指針。查詢到指針之后再去對應(yīng)地址取出數(shù)據(jù),但是這樣應(yīng)該會增加一次 I/O 吧,應(yīng)該也是在數(shù)據(jù)量和 I/O 次數(shù)之間做了取舍,具體先不討論。

以 Sqlite3.12 之后為例,page_size = 16k,假設(shè)指針為 8 byte,假設(shè)關(guān)鍵字類型占 8 byte,假設(shè)數(shù)據(jù)占 1 KB;

B 樹的一個節(jié)點:

b樹

一頁能存儲的數(shù)據(jù)量為:16kb / (1KB+8byte+8byte) ≈ 16;

高度為 3 的 B 樹能存儲 16 x16 x16 = 4096 條數(shù)據(jù)

相比于二叉樹的 1 個而言,確實有效降低了樹的層級。而且上述是假設(shè)數(shù)據(jù)為 1KB,如果數(shù)據(jù)沒那么大,高度為 3 的 B 樹能存儲更多的數(shù)據(jù),但是如果用在大型數(shù)據(jù)庫索引上還是不夠。

B+ 樹:

B+樹

如上圖,B+樹的核心在于非葉子節(jié)點不存儲數(shù)據(jù)。

這樣做可以減少非葉子結(jié)點占用的空間,增大一頁所能存儲的數(shù)據(jù)量,最大程度減少樹的層級。

仍然是以上假設(shè),假如樹的高度為 3 ,那么就有兩層存儲關(guān)鍵字+指針,一層葉子節(jié)點來存儲實際數(shù)據(jù)。

一頁能存儲的關(guān)鍵字為:16 * 1024 / (8 + 8) = 1024
一頁能存儲的數(shù)據(jù)量為:16KB / (1KB + 8byte + 8byte) = 16
(這里計算不完全準(zhǔn)確,實際情況應(yīng)該是1頁數(shù)據(jù)中只有一個鏈表指針指向下一頁)
能存儲的關(guān)鍵字為:1024 * 1024 = 1048576;

因為端節(jié)點又有 1024 個指針,這些指針可以指向一個頁,頁中存儲數(shù)據(jù),也就是葉子節(jié)點,一頁能存儲 16 個葉子節(jié)點,所以總共能索引的數(shù)據(jù)量為 1048576 * 16 ≈ 1600萬;如果高度為 4 ,則再乘以 1024 約為 17億.....

上述推理中,理解終端節(jié)點的指針指向一個頁,頁中存儲著關(guān)鍵字 + 數(shù)據(jù) + 鏈表指針是關(guān)鍵。page 標(biāo)記如下,有助理解:

理解

雖然葉子節(jié)點很多,一個 page 對應(yīng)一個葉子節(jié)點甚至是多個 page 才能存下一個葉子節(jié)點,但是這些是存在磁盤上的,找到對應(yīng)的 page 之后才去加載對應(yīng)的 page。索引超大數(shù)據(jù)量的同時,不會對 I/O 次數(shù)產(chǎn)生影響,這就是這個設(shè)計的牛逼之處。

但是這樣也是有缺點的:

無論查詢結(jié)果如何,都必須走到葉子節(jié)點才結(jié)束,也就是 I/O 次數(shù)固定為 O(h) 或者說是 log(n)(底數(shù)為節(jié)點子分支個數(shù)),這個 h 一般為 2-3,排除掉根節(jié)點常駐內(nèi)存,高度為 3 的 B+ 樹進(jìn)行兩次 I/O 就可以索引千萬級別的數(shù)據(jù),高度為 4 的 B+ 樹,進(jìn)行 3 次 I/O 就能索引十億級別的數(shù)據(jù)量,這個效果還是很好的。

所以,這個缺點也可以說成是優(yōu)點:穩(wěn)定(穩(wěn)如一條老狗??)

7. 實際應(yīng)用

  • 紅黑樹優(yōu)點

紅黑樹常用于存儲內(nèi)存中的有序數(shù)據(jù),增刪很快,內(nèi)存存儲不涉及 I/O 操作。

  • B/B+樹的優(yōu)點

更適合磁盤存儲,減少了樹的層級,進(jìn)而減少 I/O 次數(shù);

  • B 樹和 B+ 樹對比

都是 B 樹,但是 B+樹更適合范圍查詢,比如 Mysql,且查詢次數(shù)很穩(wěn)定,為 logn。而 B 樹更適合鍵值對型的聚合數(shù)據(jù)庫,比如 MongoDB,查詢次數(shù)最優(yōu)為 O(1);

紅黑樹更適合內(nèi)存存儲,B 樹更適合鍵值對存儲,B+ 樹適合范圍查詢;

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。