为什么 MySQL 使用 B+ 树,而不是 B 树或者 Hash?
為什么 MySQL 使用 B+ 樹是面試中經(jīng)常會(huì)出現(xiàn)的問題,很多人對(duì)于這個(gè)問題可能都有一些自己的理解,但是多數(shù)的回答都不夠完整和準(zhǔn)確。
大多數(shù)人都只會(huì)簡(jiǎn)單說一下 B+ 樹和 B 樹的區(qū)別,但是都沒有真正回答 MySQL 為什么選擇使用 B+ 樹這個(gè)問題,我們?cè)谶@篇文章中就會(huì)深入分析 MySQL 選擇 B+ 樹背后的一些原因。
概述
首先需要澄清的一點(diǎn)是,MySQL 跟 B+ 樹沒有直接的關(guān)系,真正與 B+ 樹有關(guān)系的是 MySQL 的默認(rèn)存儲(chǔ)引擎 InnoDB,MySQL 中存儲(chǔ)引擎的主要作用是負(fù)責(zé)數(shù)據(jù)的存儲(chǔ)和提取,除了 InnoDB 之外,MySQL 中也支持 MyISAM 作為表的底層存儲(chǔ)引擎。
我們?cè)谑褂?SQL 語(yǔ)句創(chuàng)建表時(shí)就可以為當(dāng)前表指定使用的存儲(chǔ)引擎,你能在 MySQL 的文檔 Alternative Storage Engines 中找到它支持的全部存儲(chǔ)引擎,例如:MyISAM、CSV、MEMORY?等,然而默認(rèn)情況下,使用如下所示的 SQL 語(yǔ)句來創(chuàng)建表就會(huì)得到 InnoDB 存儲(chǔ)引擎支撐的表:
CREATE?TABLE?t1 (a?INT,b?CHAR?(20 ), PRIMARY?KEY?(a))?ENGINE=InnoDB;?
想要詳細(xì)了解 MySQL 默認(rèn)存儲(chǔ)引擎的讀者,可以通過之前的文章 『淺入淺出』MySQL 和 InnoDB 了解包括 InnoDB 存儲(chǔ)方式、索引和鎖等內(nèi)容,我們?cè)谶@里主要不會(huì)介紹 InnoDB 相關(guān)的過多內(nèi)容。
我們今天最終將要分析的問題其實(shí)還是,為什么 MySQL 默認(rèn)的存儲(chǔ)引擎 InnoDB 會(huì)使用 MySQL 來存儲(chǔ)數(shù)據(jù),相信對(duì) MySQL 稍微有些了解的人都知道。
無(wú)論是表中的數(shù)據(jù)(主鍵索引)還是輔助索引最終都會(huì)使用 B+ 樹來存儲(chǔ)數(shù)據(jù),其中前者在表中會(huì)以?<id, row>?的方式存儲(chǔ),而后者會(huì)以?<index, id>?的方式進(jìn)行存儲(chǔ),這其實(shí)也比較好理解:
-
在主鍵索引中,id?是主鍵,我們能夠通過?id?找到該行的全部列;
-
在輔助索引中,索引中的幾個(gè)列構(gòu)成了鍵,我們能夠通過索引中的列找到?id,如果有需要的話,可以再通過?id?找到當(dāng)前數(shù)據(jù)行的全部?jī)?nèi)容;
對(duì)于 InnoDB 來說,所有的數(shù)據(jù)都是以鍵值對(duì)的方式存儲(chǔ)的,主鍵索引和輔助索引在存儲(chǔ)數(shù)據(jù)時(shí)會(huì)將?id?和?index?作為鍵,將所有列和?id?作為鍵對(duì)應(yīng)的值。
在具體分析 InnoDB 使用 B+ 樹背后的原因之前,我們需要為 B+ 樹找?guī)讉€(gè)『假想敵』,因?yàn)槿绻覀冎挥幸粋€(gè)選擇,那么選擇 B+ 樹也并不值得討論,找到的兩個(gè)假想敵就是 B 樹和哈希,相信這也是很多人會(huì)在面試中真實(shí)遇到的問題,我們就以這兩種數(shù)據(jù)結(jié)構(gòu)為例,分析比較 B+ 樹的優(yōu)點(diǎn)。
設(shè)計(jì)
到了這里我們已經(jīng)明確了今天待討論的問題,也就是為什么 MySQL 的 InnoDB 存儲(chǔ)引擎會(huì)選擇 B+ 樹作為底層的數(shù)據(jù)結(jié)構(gòu),而不選擇 B 樹或者哈希?在這一節(jié)中,我們將通過以下的兩個(gè)方面介紹 InnoDB 這樣選擇的原因。
-
InnoDB 需要支持的場(chǎng)景和功能需要在特定查詢上擁有較強(qiáng)的性能;
-
CPU 將磁盤上的數(shù)據(jù)加載到內(nèi)存中需要花費(fèi)大量的時(shí)間,這使得 B+ 樹成為了非常好的選擇;
數(shù)據(jù)的持久化以及持久化數(shù)據(jù)的查詢其實(shí)是一個(gè)常見的需求,而數(shù)據(jù)的持久化就需要我們與磁盤、內(nèi)存和 CPU 打交道;MySQL 作為 OLTP 的數(shù)據(jù)庫(kù)不僅需要具備事務(wù)的處理能力,而且要保證數(shù)據(jù)的持久化并且能夠有一定的實(shí)時(shí)數(shù)據(jù)查詢能力,這些需求共同決定了 B+ 樹的選擇,接下來我們會(huì)詳細(xì)分析上述兩個(gè)原因背后的邏輯。
讀寫性能
很多人對(duì) OLTP 這個(gè)詞可能不是特別了解,我們幫助各位讀者快速理解一下,與 OLTP 相比的還有 OLAP,它們分別是 Online Transaction Processing 和 Online Analytical Processing,從這兩個(gè)名字中我們就可以看出,前者指的就是傳統(tǒng)的關(guān)系型數(shù)據(jù)庫(kù),主要用于處理基本的、日常的事務(wù)處理,而后者主要在數(shù)據(jù)倉(cāng)庫(kù)中使用,用于支持一些復(fù)雜的分析和決策。
作為支撐 OLTP 數(shù)據(jù)庫(kù)的存儲(chǔ)引擎,我們經(jīng)常會(huì)使用 InnoDB 完成以下的一些工作:
-
通過?INSERT、UPDATE?和?DELETE?語(yǔ)句對(duì)表中的數(shù)據(jù)進(jìn)行增加、修改和刪除;
-
通過?UPDATE?和?DELETE?語(yǔ)句對(duì)符合條件的數(shù)據(jù)進(jìn)行批量的刪除;
-
通過?SELECT?語(yǔ)句和主鍵查詢某條記錄的全部列;
-
通過?SELECT?語(yǔ)句在表中查詢符合某些條件的記錄并根據(jù)某些字段排序;
-
通過?SELECT?語(yǔ)句查詢表中數(shù)據(jù)的行數(shù);
-
通過唯一索引保證表中某個(gè)字段或者某幾個(gè)字段的唯一性;
如果我們使用 B+ 樹作為底層的數(shù)據(jù)結(jié)構(gòu),那么所有只會(huì)訪問或者修改一條數(shù)據(jù)的 SQL 的時(shí)間復(fù)雜度都是?O(log n),也就是樹的高度,但是使用哈希卻有可能達(dá)到?O(1)?的時(shí)間復(fù)雜度,看起來是不是特別的美好。但是當(dāng)我們使用如下所示的 SQL 時(shí),哈希的表現(xiàn)就不會(huì)這么好了:
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'如果我們使用哈希作為底層的數(shù)據(jù)結(jié)構(gòu),遇到上述的場(chǎng)景時(shí),使用哈希構(gòu)成的主鍵索引或者輔助索引可能就沒有辦法快速處理了,它對(duì)于處理范圍查詢或者排序性能會(huì)非常差,只能進(jìn)行全表掃描并依次判斷是否滿足條件。
全表掃描對(duì)于數(shù)據(jù)庫(kù)來說是一個(gè)非常糟糕的結(jié)果,這其實(shí)也就意味著我們使用的數(shù)據(jù)結(jié)構(gòu)對(duì)于這些查詢沒有其他任何效果,最終的性能可能都不如從日志中順序進(jìn)行匹配。
使用 B+ 樹其實(shí)能夠保證數(shù)據(jù)按照鍵的順序進(jìn)行存儲(chǔ),也就是相鄰的所有數(shù)據(jù)其實(shí)都是按照自然順序排列的,使用哈希卻無(wú)法達(dá)到這樣的效果,因?yàn)楣:瘮?shù)的目的就是讓數(shù)據(jù)盡可能被分散到不同的桶中進(jìn)行存儲(chǔ),所以在遇到可能存在相同鍵?author = 'draven?或者排序以及范圍查詢?comments_count > 10?時(shí),由哈希作為底層數(shù)據(jù)結(jié)構(gòu)的表可能就會(huì)面對(duì)數(shù)據(jù)庫(kù)查詢的噩夢(mèng) —— 全表掃描。
B 樹和 B+ 樹在數(shù)據(jù)結(jié)構(gòu)上其實(shí)有一些類似,它們都可以按照某些順序?qū)λ饕械膬?nèi)容進(jìn)行遍歷,對(duì)于排序和范圍查詢等操作,B 樹和 B+ 樹相比于哈希會(huì)帶來更好的性能,當(dāng)然如果索引建立不夠好或者 SQL 查詢非常復(fù)雜,依然會(huì)導(dǎo)致全表掃描。
與 B 樹和 B+ 樹相比,哈希作為底層的數(shù)據(jù)結(jié)構(gòu)的表能夠以?O(1)?的速度處理單個(gè)數(shù)據(jù)行的增刪改查,但是面對(duì)范圍查詢或者排序時(shí)就會(huì)導(dǎo)致全表掃描的結(jié)果,而 B 樹和 B+ 樹雖然在單數(shù)據(jù)行的增刪查改上需要?O(log n)?的時(shí)間,但是它會(huì)將索引列相近的數(shù)據(jù)按順序存儲(chǔ),所以能夠避免全表掃描。
數(shù)據(jù)加載
既然使用哈希無(wú)法應(yīng)對(duì)我們常見的 SQL 中排序和范圍查詢等操作,而 B 樹和 B 樹和 B+ 樹都可以相對(duì)高效地執(zhí)行這些查詢,那么為什么我們不選擇 B 樹呢?這個(gè)原因其實(shí)非常簡(jiǎn)單 —— 計(jì)算機(jī)在讀寫文件時(shí)會(huì)以頁(yè)為單位將數(shù)據(jù)加載到內(nèi)存中。頁(yè)的大小可能會(huì)根據(jù)操作系統(tǒng)的不同而發(fā)生變化,不過在大多數(shù)的操作系統(tǒng)中,頁(yè)的大小都是?4KB,你可以通過如下的命令獲取操作系統(tǒng)上的頁(yè)大小:
$?getconf PAGE_SIZE 4096?
作者使用 macOS 系統(tǒng)的頁(yè)大小就是?4KB,當(dāng)然在不同的計(jì)算機(jī)上得到不同的結(jié)果是完全有可能的。
當(dāng)我們需要在數(shù)據(jù)庫(kù)中查詢數(shù)據(jù)時(shí),CPU 會(huì)發(fā)現(xiàn)當(dāng)前數(shù)據(jù)位于磁盤而不是內(nèi)存中,這時(shí)就會(huì)觸發(fā) I/O 操作將數(shù)據(jù)加載到內(nèi)存中進(jìn)行訪問,數(shù)據(jù)的加載都是以頁(yè)的維度進(jìn)行加載的,然而將數(shù)據(jù)從磁盤讀取到內(nèi)存中所需要的成本是非常大的,普通磁盤(非 SSD)加載數(shù)據(jù)需要經(jīng)過隊(duì)列、尋道、旋轉(zhuǎn)以及傳輸?shù)倪@些過程,大概要花費(fèi)?10ms?左右的時(shí)間。
我們?cè)诠浪?MySQL 的查詢時(shí)就可以使用?10ms?這個(gè)數(shù)量級(jí)對(duì)隨機(jī) I/O 占用的時(shí)間進(jìn)行估算,這里想要說的是隨機(jī) I/O 對(duì)于 MySQL 的查詢性能影響會(huì)非常大,而順序讀取磁盤中的數(shù)據(jù)時(shí)速度可以達(dá)到 40MB/s,這兩者的性能差距有幾個(gè)數(shù)量級(jí),由此我們也應(yīng)該盡量減少隨機(jī) I/O 的次數(shù),這樣才能提高性能。
B 樹與 B+ 樹的最大區(qū)別就是,B 樹可以在非葉結(jié)點(diǎn)中存儲(chǔ)數(shù)據(jù),但是 B+ 樹的所有數(shù)據(jù)其實(shí)都存儲(chǔ)在葉子節(jié)點(diǎn)中,當(dāng)一個(gè)表底層的數(shù)據(jù)結(jié)構(gòu)是 B 樹時(shí),假設(shè)我們需要訪問所有『大于 4,并且小于 9 的數(shù)據(jù)』:
如果不考慮任何優(yōu)化,在上面的簡(jiǎn)單 B 樹中我們需要進(jìn)行 4 次磁盤的隨機(jī) I/O 才能找到所有滿足條件的數(shù)據(jù)行:
加載根節(jié)點(diǎn)所在的頁(yè),發(fā)現(xiàn)根節(jié)點(diǎn)的第一個(gè)元素是 6,大于 4;
通過根節(jié)點(diǎn)的指針加載左子節(jié)點(diǎn)所在的頁(yè),遍歷頁(yè)面中的數(shù)據(jù),找到 5;
重新加載根節(jié)點(diǎn)所在的頁(yè),發(fā)現(xiàn)根節(jié)點(diǎn)不包含第二個(gè)元素;
通過根節(jié)點(diǎn)的指針加載右子節(jié)點(diǎn)所在的頁(yè),遍歷頁(yè)面中的數(shù)據(jù),找到 7 和 8;
當(dāng)然我們可以通過各種方式來對(duì)上述的過程進(jìn)行優(yōu)化,不過 B 樹能做的優(yōu)化 B+ 樹基本都可以,所以我們不需要考慮優(yōu)化 B 樹而帶來的收益,直接來看看什么樣的優(yōu)化 B+ 樹可以做,而 B 樹不行。
由于所有的節(jié)點(diǎn)都可能包含目標(biāo)數(shù)據(jù),我們總是要從根節(jié)點(diǎn)向下遍歷子樹查找滿足條件的數(shù)據(jù)行,這個(gè)特點(diǎn)帶來了大量的隨機(jī) I/O,也是 B 樹最大的性能問題。
B+ 樹中就不存在這個(gè)問題了,因?yàn)樗械臄?shù)據(jù)行都存儲(chǔ)在葉節(jié)點(diǎn)中,而這些葉節(jié)點(diǎn)可以通過『指針』依次按順序連接,當(dāng)我們?cè)谌缦滤镜?B+ 樹遍歷數(shù)據(jù)時(shí)可以直接在多個(gè)子節(jié)點(diǎn)之間進(jìn)行跳轉(zhuǎn),這樣能夠節(jié)省大量的磁盤 I/O 時(shí)間,也不需要在不同層級(jí)的節(jié)點(diǎn)之間對(duì)數(shù)據(jù)進(jìn)行拼接和排序;通過一個(gè) B+ 樹最左側(cè)的葉子節(jié)點(diǎn),我們可以像鏈表一樣遍歷整個(gè)樹中的全部數(shù)據(jù),我們也可以引入雙向鏈表保證倒序遍歷時(shí)的性能
有些讀者可能會(huì)認(rèn)為使用 B+ 樹這種數(shù)據(jù)結(jié)構(gòu)會(huì)增加樹的高度從而增加整體的耗時(shí),然而高度為 3 的 B+ 樹就能夠存儲(chǔ)千萬(wàn)級(jí)別的數(shù)據(jù),實(shí)踐中 B+ 樹的高度最多也就 4 或者 5,所以這并不是影響性能的根本問題。
總結(jié)
任何不考慮應(yīng)用場(chǎng)景的設(shè)計(jì)都不是最好的設(shè)計(jì),當(dāng)我們明確的定義了使用 MySQL 時(shí)的常見查詢需求并理解場(chǎng)景之后,再對(duì)不同的數(shù)據(jù)結(jié)構(gòu)進(jìn)行選擇就成了理所當(dāng)然的事情,當(dāng)然 B+ 樹可能無(wú)法對(duì)所有 OLTP 場(chǎng)景下的查詢都有著較好的性能,但是它能夠解決大多數(shù)的問題。
我們?cè)谶@里重新回顧一下 MySQL 默認(rèn)的存儲(chǔ)引擎選擇 B+ 樹而不是哈希或者 B 樹的原因:
-
哈希雖然能夠提供?O(1)?的單數(shù)據(jù)行操作性能,但是對(duì)于范圍查詢和排序卻無(wú)法很好地支持,最終導(dǎo)致全表掃描;
-
B 樹能夠在非葉節(jié)點(diǎn)中存儲(chǔ)數(shù)據(jù),但是這也導(dǎo)致在查詢連續(xù)數(shù)據(jù)時(shí)可能會(huì)帶來更多的隨機(jī) I/O,而 B+ 樹的所有葉節(jié)點(diǎn)可以通過指針相互連接,能夠減少順序遍歷時(shí)產(chǎn)生的額外隨機(jī) I/O;
如果想要追求各方面的極致性能也不是沒有可能,只是會(huì)帶來更高的復(fù)雜度,我們可以為一張表同時(shí)建 B+ 樹和哈希構(gòu)成的存儲(chǔ)結(jié)構(gòu),這樣不同類型的查詢就可以選擇相對(duì)更快的數(shù)據(jù)結(jié)構(gòu),但是會(huì)導(dǎo)致更新和刪除時(shí)需要操作多份數(shù)據(jù)。
從今天的角度來看,B+ 樹可能不是 InnoDB 的最優(yōu)選擇,但是它一定是能夠滿足當(dāng)時(shí)設(shè)計(jì)場(chǎng)景的需要,從 B+ 樹作為數(shù)據(jù)庫(kù)底層的存儲(chǔ)結(jié)構(gòu)到今天已經(jīng)過了幾十年的時(shí)間,我們不得不說優(yōu)秀的工程設(shè)計(jì)確實(shí)有足夠的生命力。而我們作為工程師,在選擇數(shù)據(jù)庫(kù)時(shí)也應(yīng)該非常清楚地知道不同數(shù)據(jù)庫(kù)適合的場(chǎng)景,因?yàn)檐浖こ讨袥]有銀彈。
到最后,我們還是來看一些比較開放的相關(guān)問題,有興趣的讀者可以仔細(xì)思考一下下面的問題:
-
常用于分析的 OLAP 數(shù)據(jù)庫(kù)一般會(huì)使用什么樣的數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)數(shù)據(jù)?為什么?
-
Redis 是如何對(duì)數(shù)據(jù)進(jìn)行持久化存儲(chǔ)的?常見的數(shù)據(jù)結(jié)構(gòu)都有什么?
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
總結(jié)
以上是生活随笔為你收集整理的为什么 MySQL 使用 B+ 树,而不是 B 树或者 Hash?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 可能是第二好的 Spring OAuth
- 下一篇: 为什么大家都说 SELECT * 效率低