爲何 MySQL 使用 B+ 樹是面試中常常會出現的問題,不少人對於這個問題可能都有一些本身的理解,可是多數的回答都不夠完整和準確,大多數人都只會簡單說一下 B+ 樹和 B 樹的區別,可是都沒有真正回答 MySQL 爲何選擇使用 B+ 樹這個問題,咱們在這篇文章中就會深刻分析 MySQL 選擇 B+ 樹背後的一些緣由。git
概述github
首先須要澄清的一點是,MySQL 跟 B+ 樹沒有直接的關係,真正與 B+ 樹有關係的是 MySQL 的默認存儲引擎 InnoDB,MySQL 中存儲引擎的主要做用是負責數據的存儲和提取,除了 InnoDB 以外,MySQL 中也支持 MyISAM 做爲表的底層存儲引擎。面試
咱們在使用 SQL 語句建立表時就能夠爲當前表指定使用的存儲引擎,你能在 MySQL 的文檔 Alternative Storage Engines 中找到它支持的所有存儲引擎,例如:MyISAM、CSV、MEMORY 等,然而默認狀況下,使用以下所示的 SQL 語句來建立表就會獲得 InnoDB 存儲引擎支撐的表:sql
CREATE TABLE t1 (
a INT,
b CHAR (20
), PRIMARY KEY (a)) ENGINE=InnoDB;
想要詳細瞭解 MySQL 默認存儲引擎的讀者,能夠經過以前的文章 『淺入淺出』MySQL 和 InnoDB 瞭解包括 InnoDB 存儲方式、索引和鎖等內容,咱們在這裏主要不會介紹 InnoDB 相關的過多內容。數據庫
咱們今天最終將要分析的問題其實仍是,爲何 MySQL 默認的存儲引擎 InnoDB 會使用 MySQL 來存儲數據,相信對 MySQL 稍微有些瞭解的人都知道,不管是表中的數據(主鍵索引)仍是輔助索引最終都會使用 B+ 樹來存儲數據,其中前者在表中會以 <id, row> 的方式存儲,然後者會以 <index, id> 的方式進行存儲,這其實也比較好理解:數據結構
在主鍵索引中,id 是主鍵,咱們可以經過 id 找到該行的所有列;函數
在輔助索引中,索引中的幾個列構成了鍵,咱們可以經過索引中的列找到 id,若是有須要的話,能夠再經過 id 找到當前數據行的所有內容;post
對於 InnoDB 來講,全部的數據都是以鍵值對的方式存儲的,主鍵索引和輔助索引在存儲數據時會將 id 和 index 做爲鍵,將全部列和 id 做爲鍵對應的值。性能
在具體分析 InnoDB 使用 B+ 樹背後的緣由以前,咱們須要爲 B+ 樹找幾個『假想敵』,由於若是咱們只有一個選擇,那麼選擇 B+ 樹也並不值得討論,找到的兩個假想敵就是 B 樹和哈希,相信這也是不少人會在面試中真實遇到的問題,咱們就以這兩種數據結構爲例,分析比較 B+ 樹的優勢。優化
設計
到了這裏咱們已經明確了今天待討論的問題,也就是爲何 MySQL 的 InnoDB 存儲引擎會選擇 B+ 樹做爲底層的數據結構,而不選擇 B 樹或者哈希?在這一節中,咱們將經過如下的兩個方面介紹 InnoDB 這樣選擇的緣由。
InnoDB 須要支持的場景和功能須要在特定查詢上擁有較強的性能;
CPU 將磁盤上的數據加載到內存中須要花費大量的時間,這使得 B+ 樹成爲了很是好的選擇;
數據的持久化以及持久化數據的查詢實際上是一個常見的需求,而數據的持久化就須要咱們與磁盤、內存和 CPU 打交道;MySQL 做爲 OLTP 的數據庫不只須要具有事務的處理能力,並且要保證數據的持久化而且可以有必定的實時數據查詢能力,這些需求共同決定了 B+ 樹的選擇,接下來咱們會詳細分析上述兩個緣由背後的邏輯。
讀寫性能
不少人對 OLTP 這個詞可能不是特別瞭解,咱們幫助各位讀者快速理解一下,與 OLTP 相比的還有 OLAP,它們分別是 Online Transaction Processing 和 Online 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/draveness' WHERE author = 'draven' DELETE FROM posts WHERE author = 'draven'
若是咱們使用哈希做爲底層的數據結構,遇到上述的場景時,使用哈希構成的主鍵索引或者輔助索引可能就沒有辦法快速處理了,它對於處理範圍查詢或者排序性能會很是差,只能進行全表掃描並依次判斷是否知足條件。全表掃描對於數據庫來講是一個很是糟糕的結果,這其實也就意味着咱們使用的數據結構對於這些查詢沒有其餘任何效果,最終的性能可能都不如從日誌中順序進行匹配。
使用 B+ 樹其實可以保證數據按照鍵的順序進行存儲,也就是相鄰的全部數據其實都是按照天然順序排列的,使用哈希卻沒法達到這樣的效果,由於哈希函數的目的就是讓數據儘量被分散到不一樣的桶中進行存儲,因此在遇到可能存在相同鍵 author = 'draven 或者排序以及範圍查詢 comments_count > 10 時,由哈希做爲底層數據結構的表可能就會面對數據庫查詢的噩夢 —— 全表掃描。
B 樹和 B+ 樹在數據結構上其實有一些相似,它們均可以按照某些順序對索引中的內容進行遍歷,對於排序和範圍查詢等操做,B 樹和 B+ 樹相比於哈希會帶來更好的性能,固然若是索引創建不夠好或者 SQL 查詢很是複雜,依然會致使全表掃描。
與 B 樹和 B+ 樹相比,哈希做爲底層的數據結構的表可以以 O(1) 的速度處理單個數據行的增刪改查,可是面對範圍查詢或者排序時就會致使全表掃描的結果,而 B 樹和 B+ 樹雖然在單數據行的增刪查改上須要 O(log n) 的時間,可是它會將索引列相近的數據按順序存儲,因此可以避免全表掃描。
數據加載
既然使用哈希沒法應對咱們常見的 SQL 中排序和範圍查詢等操做,而 B 樹和 B 樹和 B+ 樹均可以相對高效地執行這些查詢,那麼爲何咱們不選擇 B 樹呢?這個緣由其實很是簡單 —— 計算機在讀寫文件時會以頁爲單位將數據加載到內存中。頁的大小可能會根據操做系統的不一樣而發生變化,不過在大多數的操做系統中,頁的大小都是 4KB,你能夠經過以下的命令獲取操做系統上的頁大小:
$ getconf PAGE_SIZE 4096
做者使用 macOS 系統的頁大小就是 4KB,固然在不一樣的計算機上獲得不一樣的結果是徹底有可能的。
當咱們須要在數據庫中查詢數據時,CPU 會發現當前數據位於磁盤而不是內存中,這時就會觸發 I/O 操做將數據加載到內存中進行訪問,數據的加載都是以頁的維度進行加載的,然而將數據從磁盤讀取到內存中所須要的成本是很是大的,普通磁盤(非 SSD)加載數據須要通過隊列、尋道、旋轉以及傳輸的這些過程,大概要花費 10ms 左右的時間。
咱們在估算 MySQL 的查詢時就可使用 10ms 這個數量級對隨機 I/O 佔用的時間進行估算,這裏想要說的是隨機 I/O 對於 MySQL 的查詢性能影響會很是大,而順序讀取磁盤中的數據時速度能夠達到 40MB/s,這二者的性能差距有幾個數量級,由此咱們也應該儘可能減小隨機 I/O 的次數,這樣才能提升性能。
B 樹與 B+ 樹的最大區別就是,B 樹能夠在非葉結點中存儲數據,可是 B+ 樹的全部數據其實都存儲在葉子節點中,當一個表底層的數據結構是 B 樹時,假設咱們須要訪問全部『大於 4,而且小於 9 的數據』:
若是不考慮任何優化,在上面的簡單 B 樹中咱們須要進行 4 次磁盤的隨機 I/O 才能找到全部知足條件的數據行:
加載根節點所在的頁,發現根節點的第一個元素是 6,大於 4;
經過根節點的指針加載左子節點所在的頁,遍歷頁面中的數據,找到 5;
從新加載根節點所在的頁,發現根節點不包含第二個元素;
經過根節點的指針加載右子節點所在的頁,遍歷頁面中的數據,找到 7 和 8;
固然咱們能夠經過各類方式來對上述的過程進行優化,不過 B 樹能作的優化 B+ 樹基本均可以,因此咱們不須要考慮優化 B 樹而帶來的收益,直接來看看什麼樣的優化 B+ 樹能夠作,而 B 樹不行。
因爲全部的節點均可能包含目標數據,咱們老是要從根節點向下遍歷子樹查找知足條件的數據行,這個特色帶來了大量的隨機 I/O,也是 B 樹最大的性能問題。
B+ 樹中就不存在這個問題了,由於全部的數據行都存儲在葉節點中,而這些葉節點能夠經過『指針』依次按順序鏈接,當咱們在以下所示的 B+ 樹遍歷數據時能夠直接在多個子節點之間進行跳轉,這樣可以節省大量的磁盤 I/O 時間,也不須要在不一樣層級的節點之間對數據進行拼接和排序;經過一個 B+ 樹最左側的葉子節點,咱們能夠像鏈表同樣遍歷整個樹中的所有數據,咱們也能夠引入雙向鏈表保證倒序遍歷時的性能
有些讀者可能會認爲使用 B+ 樹這種數據結構會增長樹的高度從而增長總體的耗時,然而高度爲 3 的 B+ 樹就可以存儲千萬級別的數據,實踐中 B+ 樹的高度最多也就 4 或者 5,因此這並非影響性能的根本問題。
總結
任何不考慮應用場景的設計都不是最好的設計,當咱們明確的定義了使用 MySQL 時的常見查詢需求並理解場景以後,再對不一樣的數據結構進行選擇就成了理所固然的事情,固然 B+ 樹可能沒法對全部 OLTP 場景下的查詢都有着較好的性能,可是它可以解決大多數的問題。
咱們在這裏從新回顧一下 MySQL 默認的存儲引擎選擇 B+ 樹而不是哈希或者 B 樹的緣由:
哈希雖然可以提供 O(1) 的單數據行操做性能,可是對於範圍查詢和排序卻沒法很好地支持,最終致使全表掃描;
B 樹可以在非葉節點中存儲數據,可是這也致使在查詢連續數據時可能會帶來更多的隨機 I/O,而 B+ 樹的全部葉節點能夠經過指針相互鏈接,可以減小順序遍歷時產生的額外隨機 I/O;
若是想要追求各方面的極致性能也不是沒有可能,只是會帶來更高的複雜度,咱們能夠爲一張表同時建 B+ 樹和哈希構成的存儲結構,這樣不一樣類型的查詢就能夠選擇相對更快的數據結構,可是會致使更新和刪除時須要操做多份數據。
從今天的角度來看,B+ 樹可能不是 InnoDB 的最優選擇,可是它必定是可以知足當時設計場景的須要,從 B+ 樹做爲數據庫底層的存儲結構到今天已通過了幾十年的時間,咱們不得不說優秀的工程設計確實有足夠的生命力。而咱們做爲工程師,在選擇數據庫時也應該很是清楚地知道不一樣數據庫適合的場景,由於軟件工程中沒有銀彈。
到最後,咱們仍是來看一些比較開放的相關問題,有興趣的讀者能夠仔細思考一下下面的問題:
經常使用於分析的 OLAP 數據庫通常會使用什麼樣的數據結構存儲數據?爲何?
Redis 是如何對數據進行持久化存儲的?常見的數據結構都有什麼?
若是對文章中的內容有疑問或者想要了解更多軟件工程上一些設計決策背後的緣由,能夠在博客下面留言,做者會及時回覆本文相關的疑問並選擇其中合適的主題做爲後續的內容。
Reference
B+ tree · Wikipedia
What is the difference between Mysql InnoDB B+ tree index and hash index? Why does MongoDB use B-tree?
B+Trees and why I love them, part I
What are the main differences between INNODB and MYISAM
B+ Tree File Organization
Database Index: A Re-visit to B+ Tree
Fundamentals of database systems