1. 程式人生 > 其它 >MySQL 儲存引擎的索引為什麼使用B+樹?

MySQL 儲存引擎的索引為什麼使用B+樹?

MySQL 儲存引擎的索引為什麼使用B+樹?

為什麼 MySQL 使用 B+樹 - 知乎 (zhihu.com)

首先需要澄清的一點是,MySQL 跟 B+ 樹沒有直接的關係,真正與 B+ 樹有關係的是 MySQL 的預設儲存引擎 InnoDB,MySQL 中儲存引擎的主要作用是負責資料的儲存和提取(從我們的磁碟中),除了 InnoDB 之外,MySQL 中也支援 MyISAM 作為表的底層儲存引擎。

先思考一下,為什麼不用平衡二叉搜尋樹或者紅黑樹,而選擇B+樹?

  1. 二叉樹相比於順序查詢的確減少了查詢次數,但是在最壞情況下,二叉樹有可能退化為順序查詢。而且就二叉樹本身來說,當資料庫的資料量特別大時,其層數也將特別大
    。二叉樹的高度一般是 log_2^n,B 樹的高度是 log_t^((n+1)/2) + 1,其高度約比 B 樹大 lgt 倍。n 是節點總數,t 是樹的最小度數。
  2. B 樹在提高 IO 效能的同時,並沒與解決元素遍歷時效率低下的問題,正是為了解決這個問題,B+樹應運而生。B+樹只需遍歷葉子節點即可實現整棵樹的遍歷,而 B 樹必須使用中序遍歷按序掃庫,B+樹支援範圍查詢非常方便。這才是資料庫選用 B+樹的主要原因。另外,最後說一下,並不是說 B+樹就比 B 樹好,有很多基於頻率的搜尋是選用 B樹,越頻繁 query 的結點越往根上走,前提是需要對 query 做統計,而且要對 key做一些變化。無論是 B 樹還是 B+樹由於前邊幾層反覆 query,因此早已被載入入記憶體,不會出現讀磁碟 IO。一般啟動的時候,就會主動換入記憶體。在記憶體中 B+樹並沒有優勢,只有在磁碟中 B+樹的威力才能顯現。
  3. B+樹的高度一般為 2-4 層,所以查詢記錄時最多隻需要 2-4 次 IO,相對二叉平衡樹已經大大降低了。 範圍查詢時,能通過葉子節點的指標獲取資料。例如查詢大於等於 3 的資料,當在葉子節點中查到 3 時,通過 3 的尾指標便能獲取所有資料,而不需要再像二叉樹一樣再獲取到 3 的父節點。

總結一下:

  • 二叉查詢樹(BST):解決了排序的基本問題,但是由於無法保證平衡,可能退化為連結串列
  • 平衡二叉樹(AVⅥL):通過旋轉解決了平衡的問題,但是旋轉操作效率太低
  • 紅黑樹:通過捨棄嚴格的平衡和引入紅黑節點,解決了 AⅥ旋轉效率過低的問題,但是在磁碟等場景下,樹仍然太高,IO 次數太多
  • B 樹:通過將二叉樹改為多路平衡查詢樹,解決了樹過高的問題
  • B+樹:在 B 樹的基礎上,將非葉節點改造為不儲存資料的純索引節點,進一步降低了樹的高度;此外將葉節點使用指標連線成連結串列,範圍查詢更加高效。B+樹葉節點之間只是邏輯相鄰,而不是物理相鄰,甚至在物理位置相鄰很遠的情況下,依然會產生很多的隨機IOB+樹減少隨機IO的關鍵在於,利用葉節點邏輯相鄰的特性,儘可能地做到物理相鄰(資料被分配到連續的頁中),使得在讀取葉節點中的大量記錄時可以使用順序IO。這點很重要!

我們今天最終將要分析的問題其實還是,為什麼 MySQL 預設的儲存引擎 InnoDB 會使用 B+ 樹來儲存資料,相信對 MySQL 稍微有些瞭解的人都知道,無論是表中的資料(主鍵索引)還是輔助索引最終都會使用 B+ 樹來儲存資料,其中前者在表中會以 <id, row> 的方式儲存,而後者會以 <index, id> 的方式進行儲存,這其實也比較好理解:

  • 在主鍵索引中,id 是主鍵,我們能夠通過 id 找到該行的全部列;
  • 在輔助索引中,索引中的幾個列構成了鍵,我們能夠通過索引中的列找到 id,如果有需要的話,可以再通過 id 找到當前資料行的全部內容;

對於 InnoDB 來說,所有的資料都是以鍵值對的方式儲存的,主鍵索引和輔助索引在儲存資料時會將 id 和 index 作為鍵,將所有列和 id 作為鍵對應的值。

到了這裡我們已經明確了今天待討論的問題,也就是為什麼 MySQL 的 InnoDB 儲存引擎會選擇 B+ 樹作為底層的資料結構,而不選擇 B 樹或者雜湊?在這一節中,我們將通過以下的兩個方面介紹 InnoDB 這樣選擇的原因。

  • InnoDB 需要支援的場景和功能需要在特定查詢上擁有較強的效能
  • CPU 將磁碟上的資料載入到記憶體中需要花費大量的時間,這使得 B+ 樹成為了非常好的選擇;

資料的持久化以及持久化資料的查詢其實是一個常見的需求,而資料的持久化就需要我們與磁碟、記憶體和 CPU 打交道;MySQL 作為 OLTP 的資料庫不僅需要具備事務的處理能力,而且要保證資料的持久化並且能夠有一定的實時資料查詢能力,這些需求共同決定了 B+ 樹的選擇,接下來我們會詳細分析上述兩個原因背後的邏輯。

  • 讀寫效能

很多人對 OLTP 這個詞可能不是特別瞭解,我們幫助各位讀者快速理解一下,與 OLTP 相比的還有 OLAP,它們分別是 Online Transaction ProcessingOnline Analytical Processing,從這兩個名字中我們就可以看出,前者指的就是傳統的關係型資料庫,主要用於處理基本的、日常的事務處理,而後者主要在資料倉庫中使用,用於支援一些複雜的分析和決策

作為支撐 OLTP 資料庫的儲存引擎,我們經常會使用 InnoDB 完成以下的一些工作:

  • 通過 INSERT、UPDATE 和 DELETE 語句對錶中的資料進行增加、修改和刪除;
  • 通過 UPDATE 和 DELETE 語句對符合條件的資料進行批量的刪除;
  • 通過 SELECT 語句和主鍵查詢某條記錄的全部列;
  • 通過 SELECT 語句在表中查詢符合某些條件的記錄並根據某些欄位排序;
  • 通過 SELECT 語句查詢表中資料的行數;
  • 通過唯一索引保證表中某個欄位或者某幾個欄位的唯一性;

如果我們使用 B+ 樹作為底層的資料結構,那麼所有隻會訪問或者修改一條資料的 SQL 的時間複雜度都是 O(log n),也就是樹的高度,但是使用雜湊卻有可能達到 O(1) 的時間複雜度,看起來是不是特別的美好。但是當我們使用如下所示的 SQL 時,雜湊的表現就不會這麼好了:

select * from posts where author = 'draven' order by created_at desc;
select * from posts where comments_count > 10;
update posts set github = 'github.com/BearBrick0' where author = 'BearBrick0';
delete from posts where author = 'BearBrick0';

如果我們使用雜湊作為底層的資料結構,遇到上述的場景時,使用雜湊構成的主鍵索引或者輔助索引可能就沒有辦法快速處理了,它對於處理範圍查詢或者排序效能會非常差只能進行全表掃描並依次判斷是否滿足條件。全表掃描對於資料庫來說是一個非常糟糕的結果,這其實也就意味著我們使用的資料結構對於這些查詢沒有其他任何效果,最終的效能可能都不如從日誌中順序進行匹配。

使用 B+ 樹其實能夠保證資料按照鍵的順序進行儲存,也就是相鄰的所有資料其實都是按照自然順序排列的,使用雜湊卻無法達到這樣的效果,因為雜湊函式的目的就是讓資料儘可能被分散到不同的桶中進行儲存,所以在遇到可能存在相同鍵 author = 'BearBrick0 或者排序以及範圍查詢 comments_count > 10 時,由雜湊作為底層資料結構的表可能就會面對資料庫查詢的噩夢 —— 全表掃描。

B 樹和 B+ 樹在資料結構上其實有一些類似,它們都可以按照某些順序對索引中的內容進行遍歷,對於排序和範圍查詢等操作,B 樹和 B+ 樹相比於雜湊會帶來更好的效能,當然如果索引建立不夠好或者 SQL 查詢非常複雜,依然會導致全表掃描。

與 B 樹和 B+ 樹相比,雜湊作為底層的資料結構的表能夠以 O(1) 的速度處理單個數據行的增刪改查,但是面對範圍查詢或者排序時就會導致全表掃描的結果,而 B 樹和 B+ 樹雖然在單資料行的增刪查改上需要 O(log n) 的時間,但是它會將索引列相近的資料按順序儲存,所以能夠避免全表掃描。

  • 資料載入

既然使用雜湊無法應對我們常見的 SQL 中排序和範圍查詢等操作而 B 樹和 B 樹和 B+ 樹都可以相對高效地執行這些查詢,那麼為什麼我們不選擇 B 樹呢?這個原因其實非常簡單 —— 計算機在讀寫檔案時會以頁(page)為單位將資料載入到記憶體中頁的大小可能會根據作業系統的不同而發生變化,不過在大多數的作業系統中,頁的大小都是 4KB,你可以通過如下的命令獲取作業系統上的頁大小:

插入個問題:如果我們的在B+樹中去查詢4-9之間的數,需要幾次磁碟I/O呢?

如果4-9在同一頁中,除了從根節點到對應頁的過程,就一次,不在同一頁中,多次看頁數。訪問一個page,就意味著需要讀一次磁碟,然後在記憶體裡快取成一個page,下一次再訪問這個page,就不需要讀磁碟了。B+樹特點就是以page為單位讀寫磁碟,來減少磁碟IO次數。MySQL把一個page定義為16KB,是根據經驗主義,發現16KB是一個通用的最優配置。

getconf PAGE_SIZE
4096

當我們需要在資料庫中查詢資料時,CPU 會發現當前資料位於磁碟而不是記憶體中,這時就會觸發 I/O 操作將資料載入到記憶體中進行訪問,資料的載入都是以頁的維度進行載入的,然而將資料從磁碟讀取到記憶體中所需要的成本是非常大的,普通磁碟(非 SSD)載入資料需要經過佇列、尋道、旋轉以及傳輸的這些過程,大概要花費 10ms 左右的時間。

我們在估算 MySQL 的查詢時就可以使用 10ms 這個數量級對隨機 I/O 佔用的時間進行估算,這裡想要說的是隨機 I/O 對於 MySQL 的查詢效能影響會非常大,而順序(IO)讀取磁碟中的資料時速度可以達到 40MB/s,這兩者的效能差距有幾個數量級,由此我們也應該儘量減少隨機 I/O 的次數,這樣才能提高效能。

B 樹與 B+ 樹的最大區別就是,B 樹可以在非葉結點中儲存資料,但是 B+ 樹的所有資料其實都儲存在葉子節點中,當一個表底層的資料結構是 B 樹時,假設我們需要訪問所有『大於 4,並且小於 9 的資料』:

如果不考慮任何優化,在上面的簡單 B 樹中我們需要進行 4 次磁碟的隨機 I/O 才能找到所有滿足條件的資料行:

  1. 載入根節點所在的頁,發現根節點的第一個元素是 6,大於 4;
  2. 通過根節點的指標載入左子節點所在的頁,遍歷頁面中的資料,找到 5;
  3. 重新載入根節點所在的頁,發現根節點不包含第二個元素;
  4. 通過根節點的指標載入右子節點所在的頁,遍歷頁面中的資料,找到 7 和 8;

當然我們可以通過各種方式來對上述的過程進行優化,不過 B 樹能做的優化 B+ 樹基本都可以,所以我們不需要考慮優化 B 樹而帶來的收益,直接來看看什麼樣的優化 B+ 樹可以做,而 B 樹不行。

由於所有的節點都可能包含目標資料,我們總是要從根節點向下遍歷子樹查詢滿足條件的資料行,這個特點帶來了大量的隨機 I/O,也是 B 樹最大的效能問題。

B+ 樹中就不存在這個問題了,因為所有的資料行都儲存在葉節點中,而這些葉節點可以通過『指標』依次按順序連線,當我們在如下所示的 B+ 樹遍歷資料時可以直接在多個子節點之間進行跳轉,這樣能夠節省大量的磁碟 I/O 時間,也不需要在不同層級的節點之間對資料進行拼接和排序;通過一個 B+ 樹最左側的葉子節點,我們可以像連結串列一樣遍歷整個樹中的全部資料,我們也可以引入雙向連結串列保證倒序遍歷時的效能。

有些讀者可能會認為使用 B+ 樹這種資料結構會增加樹的高度從而增加整體的耗時,然而高度為 3 的 B+ 樹就能夠儲存千萬級別的資料,實踐中 B+ 樹的高度最多也就 4 或者 5,所以這並不是影響效能的根本問題。

  • 總結

我們在這裡重新回顧一下 MySQL 預設的儲存引擎選擇 B+ 樹而不是雜湊或者 B 樹的原因:

  • 雜湊雖然能夠提供 O(1) 的單資料行操作效能,但是對於範圍查詢和排序卻無法很好地支援,最終導致全表掃描;
  • B 樹能夠在非葉節點中儲存資料,但是這也導致在查詢連續資料時可能會帶來更多的隨機 I/O,而 B+ 樹的所有葉節點可以通過指標相互連線,能夠減少順序遍歷時產生的額外隨機 I/O,是一個順序 I/O;

如果想要追求各方面的極致效能也不是沒有可能,只是會帶來更高的複雜度,我們可以為一張表同時建 B+ 樹和雜湊構成的儲存結構,這樣不同型別的查詢就可以選擇相對更快的資料結構,但是會導致更新和刪除時需要操作多份資料。

從今天的角度來看,B+ 樹可能不是 InnoDB 的最優選擇,但是它一定是能夠滿足當時設計場景的需要,從 B+ 樹作為資料庫底層的儲存結構到今天已經過了幾十年的時間,我們不得不說優秀的工程設計確實有足夠的生命力。而我們作為工程師,在選擇資料庫時也應該非常清楚地知道不同資料庫適合的場景,因為軟體工程中沒有銀彈。

本文來自部落格園,作者:{BearBrick0},轉載請註明原文連結:{https://www.cnblogs.com/bearbrick0}