深入理解Mysql的B+Tree索引原理

首先,正確的創(chuàng)建合適的索引,是提升數(shù)據(jù)庫查詢性能的基礎(chǔ)。

索引是什么?

索引是為了加速對表中數(shù)據(jù)行的檢索而創(chuàng)建的一種分散存儲的數(shù)據(jù)結(jié)構(gòu)。

索引的工作機制是怎樣的?

深入理解Mysql的B+Tree索引原理

如上圖中,如果現(xiàn)在有一條sql語句 select * from teacher where id = 101,如果沒有索引的條件下,我們要找到這條記錄,我們就需要就行全表掃描,匹配id = 101的數(shù)據(jù)。如果有了索引,我們就可以快速的通過索引找到101所對應的行記錄在磁盤中的地址,再根據(jù)給定的地址取出對應的行數(shù)據(jù)。

mysql數(shù)據(jù)庫為什么要使用B+TREE作為索引的數(shù)據(jù)結(jié)構(gòu)?

對數(shù)據(jù)的加速檢索,首先想到的就是二叉樹,二叉樹的查找時間復雜度可以達到O(log2(n))。下面看一下二叉樹的存儲結(jié)構(gòu):

深入理解Mysql的B+Tree索引原理

二叉樹搜索相當于一個二分查找。二叉查找能大大提升查詢的效率,但是它有一個問題:二叉樹以第一個插入的數(shù)據(jù)作為根節(jié)點,如上圖中,如果只看右側(cè),就會發(fā)現(xiàn),就是一個線性鏈表結(jié)構(gòu)。如果我們現(xiàn)在的數(shù)據(jù)只包含1, 2, 3, 4,5, 6,就會出現(xiàn)一下情況:

深入理解Mysql的B+Tree索引原理

如果我們要查詢的數(shù)據(jù)為6則需要遍歷所有的節(jié)點才能找到6,即,相當于全表掃描,就是由于存在這種問題,所以二叉查找樹不適合用于作為索引的數(shù)據(jù)結(jié)構(gòu)。

基于這樣的推演,為了解決存在線性鏈表的問題,很容易就能夠想到平衡二叉查找樹。下面看看平衡二叉樹是怎樣的:

深入理解Mysql的B+Tree索引原理

平衡二叉查找樹定義為:節(jié)點的子節(jié)點高度差不能超過1,如上圖中的節(jié)點20,左節(jié)點高度為1,右節(jié)點高度0,差為1,所以上圖沒有違反定義,他就是一個平衡二叉樹。保證二叉樹平衡的方式為左旋,右旋等操作,至于如果左旋右旋,可以自行去搜索相關(guān)的知識。

如果上圖中平衡二叉樹保存的是id索引,現(xiàn)在要從id = 8的數(shù)據(jù),首先要把根節(jié)點加載進內(nèi)存,用8和10進行比較,發(fā)現(xiàn)8比10小,繼續(xù)加載10的左子樹。把5加載進內(nèi)存,用8和5比較,同理,加載5節(jié)點的右子樹。此時發(fā)現(xiàn)命中,現(xiàn)在要加載id為8的索引對應的數(shù)據(jù)。

怎么找到索引對應的數(shù)據(jù)呢?

索引保存數(shù)據(jù)的方式一般有兩種,第一種為在節(jié)點的數(shù)據(jù)區(qū)保存id = 8的行數(shù)據(jù)的所有數(shù)據(jù)具體內(nèi)容。另外一種方式,數(shù)據(jù)區(qū)保存的是真正保存數(shù)據(jù)的磁盤地址。

到這里,平衡二叉樹解決了存在線性鏈表的問題,數(shù)據(jù)查詢的效率好像也還可以,基本能達到O(log2(n)), 那為什么mysql不選擇這樣的數(shù)據(jù)結(jié)構(gòu)呢,他又存在什么樣的問題呢?

問題1: 搜索效率不足,一般來說,在樹結(jié)構(gòu)中,數(shù)據(jù)所處的深度,決定了搜索時的IO次數(shù)。如上圖中搜索id = 8的數(shù)據(jù),需要進行3次IO。當數(shù)據(jù)量到達幾百萬的時候,樹的高度就會很恐怖。

問題2: 查詢不不穩(wěn)定,如果查詢的數(shù)據(jù)落在根節(jié)點,只需要一次IO,如果是葉子節(jié)點或者是支節(jié)點,會需要多次IO才可以。

問題3: 節(jié)點存儲的數(shù)據(jù)內(nèi)容太少。沒有很好利用操作系統(tǒng)和磁盤數(shù)據(jù)交換特性,也沒有利用好磁盤IO的預讀能力。因為操作系統(tǒng)和磁盤之間一次數(shù)據(jù)交換是已頁為單位的,一頁 = 4K,即每次IO操作系統(tǒng)會將4K數(shù)據(jù)加載進內(nèi)存。但是,在二叉樹每個節(jié)點的結(jié)構(gòu)只保存一個關(guān)鍵字,一個數(shù)據(jù)區(qū),兩個子節(jié)點的引用,并不能夠填滿4K的內(nèi)容。幸幸苦苦做了一次的IO操作,卻只加載了一個關(guān)鍵字,在樹的高度很高,恰好又搜索的關(guān)鍵字位于葉子節(jié)點或者支節(jié)點的時候,取一個關(guān)鍵字要做很多次的IO。

那有沒有一種結(jié)構(gòu)能夠解決二叉樹的這種問題呢?

有,多路平衡查找樹:(Balance Tree):

B Tree 是一個絕對平衡樹,所有的葉子節(jié)點在同一高度,如下圖所示:

深入理解Mysql的B+Tree索引原理

B Tree有什么優(yōu)勢,又是怎么去解決一些問題的呢?

先看定義,上圖為一個2-3樹(每個節(jié)點存儲2個關(guān)鍵字,有3路),多路平衡查找樹也就是多叉的意思,從上圖中可以看出,每個節(jié)點保存的關(guān)鍵字的個數(shù)和路數(shù)關(guān)系為:

關(guān)鍵字個數(shù) = 路數(shù) – 1。

假設要從上圖中去尋找id = 28的數(shù)據(jù),B TREE 搜索過程如下:

首先把根節(jié)點加載進內(nèi)存,加載了17,35兩個關(guān)鍵字,判斷規(guī)則為:

深入理解Mysql的B+Tree索引原理

根據(jù)以上規(guī)則命中28后,接下來加載28對應的數(shù)據(jù), 就去找28對應的數(shù)據(jù)區(qū),數(shù)據(jù)區(qū)中存儲的是具體的數(shù)據(jù)或者是指向數(shù)據(jù)的指針。

為什么說這種結(jié)構(gòu)能夠解決平衡二叉樹存在的問題呢?

能夠很好的利用操作系統(tǒng)和磁盤的交互特性, MYSQL為了很好的利用磁盤的預讀能力,將頁大小為16K,即將一個節(jié)點(磁盤塊)的大小設置為16K,一次IO將一個節(jié)點(16K)內(nèi)容加載進內(nèi)存。這里,假設關(guān)鍵字類型為 int,即4字節(jié),若每個關(guān)鍵字對應的數(shù)據(jù)區(qū)也為4字節(jié),不考慮子節(jié)點引用的情況下,則上圖中的每個節(jié)點大約能夠存儲(16 * 1000)/ 8 = 2000個關(guān)鍵字,則共2001個路數(shù)。對于二叉樹,三層高度,最多可以保存7個關(guān)鍵字,而對于這種有2001路的B樹,三層高度能夠搜索的關(guān)鍵字個數(shù)遠遠的大于二叉樹。

在B TREE保證樹的平衡的過程中,每次關(guān)鍵字的變化,都會導致結(jié)構(gòu)發(fā)生很大的變化,這個過程是特別浪費時間的,所以創(chuàng)建索引一定要創(chuàng)建合適的索引,而不是把所有的字段都創(chuàng)建索引,創(chuàng)建冗余索引只會在對數(shù)據(jù)進行新增,刪除,修改時增加性能消耗。

既然B樹已經(jīng)很好的解決了問題,為什么MYSQL還要用B+TREE?

先看看B+TREE是怎樣的,B+TREE是B TREE的一個變種,在B+樹種,B樹種的路數(shù)和關(guān)鍵字的個數(shù)的關(guān)系不再成立了,B+TREE中,數(shù)據(jù)檢索規(guī)則采用的是左閉合區(qū)間,路數(shù)和關(guān)鍵個數(shù)關(guān)系為1比1,具體如下圖所示:

深入理解Mysql的B+Tree索引原理

如果上圖中是用ID做的索引,如果是搜索id = 1的數(shù)據(jù),搜索規(guī)則如下:

深入理解Mysql的B+Tree索引原理

根據(jù)如上規(guī)則,最終在葉子節(jié)點中命中數(shù)據(jù),根據(jù)葉子節(jié)點中節(jié)點1的數(shù)據(jù)區(qū)取得真正的數(shù)據(jù)。

B TREE和B+TREE區(qū)別是什么?

1、B+TREE 關(guān)鍵字的搜索采用的是左閉合區(qū)間,之所以采用左閉合區(qū)間是因為他要最好的去支持自增id,這也是mysql的設計初衷。即,如果id = 1命中,會繼續(xù)往下查找,直到找到葉子節(jié)點中的1。

2、B+TREE 根節(jié)點和支節(jié)點沒有數(shù)據(jù)區(qū),關(guān)鍵字對應的數(shù)據(jù)只保存在葉子節(jié)點中。即只有葉子節(jié)點中的關(guān)鍵字數(shù)據(jù)區(qū)才會保存真正的數(shù)據(jù)內(nèi)容或者是內(nèi)容的地址。而在B樹種,如果根節(jié)點命中,則會直接返回數(shù)據(jù)。并且在B+TREE中,葉子節(jié)點不會去保存子節(jié)點的引用。

3、B+TREE葉子節(jié)點是順序排列的,并且相鄰的節(jié)點具有順序引用的關(guān)系,如上圖中葉子節(jié)點之間有指針相連接。

MYSQL為什么最終要去選擇B+TREE?

1、B+TREE是B TREE的變種,B TREE能解決的問題,B+TREE也能夠解決(降低樹的高度,增大節(jié)點存儲數(shù)據(jù)量)

2、 B+TREE掃庫和掃表能力更強,如果我們要根據(jù)索引去進行數(shù)據(jù)表的掃描,對B TREE進行掃描,需要把整棵樹遍歷一遍,而B+TREE只需要遍歷他的所有葉子節(jié)點即可(葉子節(jié)點之間有引用)。

3、B+TREE磁盤讀寫能力更強,他的根節(jié)點和支節(jié)點不保存數(shù)據(jù)區(qū),所有根節(jié)點和支節(jié)點同樣大小的情況下,保存的關(guān)鍵字要比B TREE要多。而葉子節(jié)點不保存子節(jié)點引用。所以,B+TREE讀寫一次磁盤加載的關(guān)鍵字比B TREE更多。

4、B+TREE排序能力更強,如上面的圖中可以看出,B+TREE天然具有排序功能。

5、B+TREE查詢效率更加穩(wěn)定,每次查詢數(shù)據(jù),查詢IO次數(shù)一定是穩(wěn)定的。當然這個每個人的理解都不同,因為在B TREE如果根節(jié)點命中直接返回,確實效率更高。

MYSQL B+TREE具體落地形式

這里主要講解的是MYSQL根據(jù)B+TREE索引結(jié)構(gòu)不同的兩種存儲引擎(MYISAM 和 INNODB)的實現(xiàn),首先找到MYSQL保存數(shù)據(jù)的文件夾,看看mysql是如何保存數(shù)據(jù)的:

深入理解Mysql的B+Tree索引原理

進入到這個目錄下,這個目錄下保存的是所有數(shù)據(jù)庫,再進入到具體的一個數(shù)據(jù)庫目錄下。在這里,有多種數(shù)據(jù)的存儲引擎,這里講解MYISAM和innodb,如圖中所示:

深入理解Mysql的B+Tree索引原理

MYISAM存儲引擎索引:

從圖中可以看出,使用MYISAM存儲引擎存儲數(shù)據(jù)庫數(shù)據(jù),一共有三個文件:

Frm,表的定義文件。MYD:數(shù)據(jù)文件,所有的數(shù)據(jù)保存在這個文件中。MYI:索引文件。

在MYISAM存儲引擎中,數(shù)據(jù)和索引的關(guān)系如下:

深入理解Mysql的B+Tree索引原理

如何查找數(shù)據(jù)的呢?如果要查詢id = 101的數(shù)據(jù),先根據(jù)MYI索引文件(如上圖左)去找id = 101的節(jié)點,通過這個節(jié)點的數(shù)據(jù)區(qū)拿到真正保存數(shù)據(jù)的磁盤地址,再通過這個地址從MYD數(shù)據(jù)文件(如上圖右)中加載對應的記錄。

如果有多個索引,表現(xiàn)形式如下:

深入理解Mysql的B+Tree索引原理

所以在MYISAM存儲引擎中,主鍵索引和輔助索引是同級別的,沒有主次之分。

Innodb存儲引擎:

首先看一下聚集索引的概念,聚集索引定義為:數(shù)據(jù)庫表行中數(shù)據(jù)的物理順序和鍵值的邏輯順序相同。

Innodb以主鍵為索引來聚集組織數(shù)據(jù)的存儲,下面看看Innodb是如何組織數(shù)據(jù)的。

Innodb只有兩個文件,F(xiàn)rm文件: 表的定義文件,和Ibd文件,沒有專門保存數(shù)據(jù)的文件。數(shù)據(jù)以主鍵進行聚集存儲,把真正的數(shù)據(jù)保存在葉子節(jié)點中。innodb設計初衷認為主鍵才是最主要的索引。具體如下圖所示:

深入理解Mysql的B+Tree索引原理

如上圖中,葉子節(jié)點的數(shù)據(jù)區(qū)保存的就是真實的數(shù)據(jù),在通過索引進行檢索的時候,命中葉子節(jié)點,就可以直接從葉子節(jié)點中取出行數(shù)據(jù)。mysql5.5版本之前采用的是MYISAM引擎,5.5之后采用的是innodb引擎。

在innodb中,輔助索引的格式如下圖所示?

深入理解Mysql的B+Tree索引原理

如上圖,主鍵索引的葉子節(jié)點保存的是真正的數(shù)據(jù)。而輔助索引葉子節(jié)點的數(shù)據(jù)區(qū)保存的是主鍵索引關(guān)鍵字的值。搜索過程為:假如要查詢name = seven的數(shù)據(jù),先在輔助索引中查詢最后找到主鍵id = 101,再在主鍵索引中搜索id為101的數(shù)據(jù),最終在主鍵索引的葉子節(jié)點中獲取到真正的數(shù)據(jù)。所以通過輔助索引進行檢索,需要檢索兩次索引。

把Innodb 和 MYISAM區(qū)別放在一張圖中看,就如下所示:

深入理解Mysql的B+Tree索引原理

創(chuàng)建索引的幾大原則:

1、列的離散型:

離散型的計算公式:count(distinct col):count(col),離散型越高,選擇型越好。

如下表中各個字段,哪一列的離散型最好:

深入理解Mysql的B+Tree索引原理

上圖中,顯然可以看出,name的離散型最好,如果用sex創(chuàng)建索引:

為什么說離散型越高,選擇型越好?

如下圖,如果對Sex創(chuàng)建索引,則索引結(jié)構(gòu)將會如下:

深入理解Mysql的B+Tree索引原理

如果此時檢索 sex = 1的數(shù)據(jù),根節(jié)點判斷的時候,結(jié)果是查詢左子樹,但是當在左子樹第二層再進行判斷的時候,因為左右分支都滿足條件,所以很難抉擇選擇哪一個分支繼續(xù)搜索,或者是把兩個分支同時進行搜索。

2、最左匹配原則

對于索引中的關(guān)鍵字進行對比的時候,一定是從左往右以此對比,且不可跳過。之前講解的id都為int型數(shù)據(jù),如果id為字符串的時候,如下圖:

深入理解Mysql的B+Tree索引原理

當進行匹配的時候,會把字符串轉(zhuǎn)換成ascll碼,如abc變成97 98 99,然后從左往右一個字符一個字符進行對比。所以在sql查詢中使用like %a 時候索引會失效,因為%表示全匹配,如果已經(jīng)全匹配就不需要索引,還不如直接全表掃描。

3、最少空間原則

前面已經(jīng)說過,當關(guān)鍵字占用的空間越小,則每個節(jié)點保存的關(guān)鍵字個數(shù)就越多,每次加載進內(nèi)存的關(guān)鍵字個數(shù)就越多,檢索效率就越高。

聯(lián)合索引:

單列索引:節(jié)點中的關(guān)鍵字[name]

聯(lián)合索引:節(jié)點中的關(guān)鍵字[name, phoneNum]

可以把單列索引看成特殊的聯(lián)合索引,聯(lián)合索引的比較也是根據(jù)最左匹配原則。

聯(lián)合索引列的選擇原則:

(1) 經(jīng)常用的列優(yōu)先(最左匹配原則)

(2) 離散度高的列優(yōu)先(離散度高原則)

(3) 寬度小的列優(yōu)先,(最少空間原則)

下面簡單舉例平時經(jīng)常會遇到的問題:

如,平時經(jīng)常使用的查詢sql如下:

Select * from users where name = ?

Select * from users where name = ? and pahoneNum = ?

為了加快檢索速度,為上面的查詢sql創(chuàng)建索引如下:

Create index idx_name on users(name)

Create index idx_name_phoneNum on users(name, phoneNum)

在上面解決方案中,根據(jù)最左匹配原則,idx_name為冗余索引, where name = ?同樣可以利用索引idx_name_phoneNum進行檢索。冗余索引會增減維護B+TREE平衡時的性能消耗,并且占用磁盤空間。

覆蓋索引:

如果查詢的列,通過索引項的信息可直接返回,則該索引稱之為查詢SQL的覆蓋索引。覆蓋索引可以提高查詢的效率。

下面通過例子說明覆蓋索引。

表:teacher

索引:PK(id), key(name, phoneNum), unique(teacherNo)

下面哪些sql使用到了覆蓋索引?

Select teacherNo from teacher where teacherNo = ?:使用到了,檢索到teacherNo 時候,可以直接將索引中的teacherNo 值返回,不需要進入數(shù)據(jù)區(qū)。

Select id,teacherNo from teacher where teacherNo = ?:使用到了,輔助索引的葉子節(jié)點保存了主索引的值,所以檢索到輔助索引的葉子節(jié)點的時候就可以之間返回id。

Select name,phoneNum from teacher where teacherNo = ?:沒有用到

Select phoneNum from teacher where name = ?, 使用到了。

知道了覆蓋索引,就知道了為什么sql中要求盡量不要使用select *,要寫明具體要查詢的字段,一個原因就是,這樣在使用到覆蓋索引的情況下,不需要進入到數(shù)據(jù)區(qū),數(shù)據(jù)就能直接返回,提升了查詢效率。

通過前面的學習,我們可以很容易的明白如下一下結(jié)論:

1、索引列的數(shù)據(jù)長度滿足業(yè)務的情況下能少則少。

2、表中的索引并不是越多越好。

3、Where 條件中,like 9%, like %9%, like%9,三種方式都用不到索引。后兩種方式對于索引是無效的。第一種9%是不確定的,決定于列的離散型,結(jié)論上講可以用到,如果發(fā)現(xiàn)離散情況特別差的情況下,查詢優(yōu)化器覺得走索引查詢性能更差,還不如全表掃描。

4、Where條件中 NOT IN 無法使用索引

5、多用指定查詢,只返回自己想要的列,少用select *。

6、查詢條件中使用函數(shù),索引將會失效,這和列的離散型有關(guān),一旦使用到函數(shù),函數(shù)具有不確定性。

7、聯(lián)合索引中,如果不是按照索引最左列開始查找,無法使用索引。

8、對聯(lián)合索引精確匹配最左前列并范圍匹配另一列,可以使用到索引。

9、聯(lián)合索引中,如果查詢有某個列的范圍查詢,其右邊所有的列都無法使用索引。

推薦Mysql教程《Mysql教程

? 版權(quán)聲明
THE END
喜歡就支持一下吧
點贊7 分享