在絕大多數(shù)情況下,Mysql索引都是基于B+樹的,而索引可以提高數(shù)據(jù)查詢的效率。
但是Mysql是如何利用B+樹進(jìn)行查詢的呢?索引的作用只是提高查詢效率嗎?
Mysql中的B+Tree索引
假設(shè)有一張教師表,里面有教師編號(hào)、名字、學(xué)科、薪資四個(gè)字段。
當(dāng)你執(zhí)行下面這條創(chuàng)建索引的sql語句時(shí):
create index id_name on teacher(name);
Mysql就會(huì)在磁盤中構(gòu)建這樣一顆B+樹:

這樣一棵樹有什么用呢?首先當(dāng)然是加速查詢。
舉個(gè)簡(jiǎn)單的例子,假設(shè)現(xiàn)在要查找名字為“Mozart”的教師的數(shù)據(jù):
select * from teacher where name = "Mozart";
既然我們已經(jīng)建立了B+樹,那么就要好好利用它來加速查詢,而不是傻傻的去遍歷整張表。
從根節(jié)點(diǎn)開始,我們發(fā)現(xiàn),根節(jié)點(diǎn)就是"Mozart",不過很可惜,根節(jié)點(diǎn)上面只有name字段的信息,沒有其他字段的數(shù)據(jù)。
這是B+樹的一個(gè)特點(diǎn)——只有葉子節(jié)點(diǎn)(leaf nodes)會(huì)指向行數(shù)據(jù)。
我們比較了要查找的值和搜索碼值,發(fā)現(xiàn)相等,于是跳到搜索碼右邊的指針指向的節(jié)點(diǎn),也就是“Srinivasan”所在的節(jié)點(diǎn)(注意,這里的節(jié)點(diǎn)是指下圖紅色框畫出的區(qū)域)。

接著,我們遍歷當(dāng)前節(jié)點(diǎn)的搜索碼值,和要查找的值做比較。
我們發(fā)現(xiàn)“Srinivasan”已經(jīng)大于我們要查找的"Mozart"了,于是就此止步,跟隨著“Srinivasan”左邊的指針,跳到下一級(jí)的節(jié)點(diǎn)。

接著,還是一樣,我們繼續(xù)遍歷當(dāng)前節(jié)點(diǎn)的搜索碼值,和要查找的值做比較。
這時(shí)我們又碰到了一個(gè)搜索碼值為"Mozart"的塊,和上次不同的是,這次是在葉子節(jié)點(diǎn)找到的,而不是根節(jié)點(diǎn)。葉子節(jié)點(diǎn)的指針指向行數(shù)據(jù)。
于是,我們循著"Mozart"左邊指針的指引,找到了"Mozart"的行數(shù)據(jù)。

當(dāng)然,這只是最最簡(jiǎn)潔的描述,如果name沒有加唯一索引,那么mysql還需要遍歷下一個(gè)塊,看看搜索碼值是不是也是"Mozart"。另外,葉子節(jié)點(diǎn)也不會(huì)直接存儲(chǔ)行數(shù)據(jù)的位置,而是存儲(chǔ)聚簇索引(clustered index)的值,通過聚簇索引去找到數(shù)據(jù)的位置,這個(gè)在后面會(huì)解釋。
通過上面的描述,大家大概對(duì)B+樹的查找原則有了一定的了解:
- 從節(jié)點(diǎn)最左邊的搜索碼值開始,向右遍歷
- 如果搜索碼值大于被查找值,則跳到搜索碼值左邊指針指向的節(jié)點(diǎn)
- 如果等于,則跳到右邊指針指向的節(jié)點(diǎn)
- 如果小于,則遍歷下一個(gè)搜索碼值
- 如果遍歷完了整個(gè)節(jié)點(diǎn),還是沒發(fā)現(xiàn)有大于等于被查找值的搜索碼,則跳到該節(jié)點(diǎn)最后一個(gè)非空指針指向的節(jié)點(diǎn)
- 不斷循環(huán),直到找到被查找值,或者發(fā)現(xiàn)被查找值不存在
作為測(cè)驗(yàn),大家可以模擬上面查找"Mozart"的過程,試著查找"Brandt"和“El Said”。
復(fù)合索引
上面講的只是單索引,那么如果是復(fù)合索引呢?
create index id_name_subject on teacher(name, subject);
一樣的,只是這次的搜索碼值,不再只是存放name一個(gè)字段,而是存放了name和subject兩個(gè)字段。
熟悉Java的同學(xué),可以理解為,之前只是一個(gè)字符串,現(xiàn)在變成了一個(gè)Object了。
之前只是單純的字符串比較,現(xiàn)在是對(duì)象間的比較。
對(duì)象怎么比較呢?一項(xiàng)項(xiàng)來,如果前一項(xiàng)分不出勝負(fù),那么再比下一項(xiàng)。
比較的順序,就是你索引創(chuàng)建語句里寫的順序。
比如按照上面那條sql創(chuàng)建出來的索引,mysql會(huì)先比較name,如果name一樣,再比較subject。
其他查找原則,和單索引一致。
最左前綴匹配
弄懂了單索引和復(fù)合索引的原理,再來理解Mysql中經(jīng)常被提及的——最左前綴匹配(leftmost prefix),就輕松的多了。
什么是最左前綴匹配?簡(jiǎn)單說,就是你給一個(gè)表的a,b,c三個(gè)字段建了索引:
create index id_a_b_c on foo(a, b, c);
那么當(dāng)你where條件是a或者a、b或者a、b、c時(shí),都可以命中索引,除此之外,都不能命中索引,比如a、c,或者b、c等。
為什么?看看上面的單索引和復(fù)合索引就知道了。
有一個(gè)例外,當(dāng)你select的字段里有復(fù)合索引里的字段,那么where語句不需要滿足最左前綴匹配,Mysql也會(huì)走索引。
比如:
select a from foo where b = "xxx";
不過這時(shí)走索引不是為了加速查詢(這時(shí)候索引對(duì)查詢效率提升效果幾乎沒有),而是為了利用下面要講的,覆蓋索引,來減少對(duì)數(shù)據(jù)的檢索。
覆蓋索引
覆蓋索引(covering index)的原理很簡(jiǎn)單,就像你拿到了一本書的目錄,里頭有標(biāo)題和對(duì)應(yīng)的頁碼,當(dāng)你想知道第267頁的標(biāo)題是什么的時(shí)候,完全沒有必要翻到267頁去看,而是直接看目錄。
同理,當(dāng)你要select的字段,已經(jīng)在索引樹里面存儲(chǔ),那就不需要再去檢索數(shù)據(jù)庫,直接拿來用就行了。
還是上面的例子,你給a、b、c三個(gè)字段建了復(fù)合索引,那么對(duì)于下面這條sql,就可以走覆蓋索引:
select b,c from foo where a = "xxx";
explain一下,你就會(huì)發(fā)現(xiàn)extra字段是“Using index”,或者使用explain FORMAT=JSON ... ,輸出一個(gè)json結(jié)果的結(jié)果,看“using_index”屬性,你會(huì)發(fā)現(xiàn)是“true”,這都意味著使用到了覆蓋索引。
Using index (JSON property: using_index)
The column information is retrieved from the table using only information in the index tree without having to do an additional seek to read the actual row.
聚簇索引和二級(jí)索引
現(xiàn)在問一個(gè)問題,下面這條sql,會(huì)走覆蓋索引嗎?還是需要去磁盤再一次檢索?
select id,b,c from foo where a = "xxx";
和上一條sql對(duì)比,這一次我們?cè)趕elect里頭,加了一個(gè)字段,主鍵id。
有同學(xué)說,id不在復(fù)合索引里,B+樹沒有id的信息,只能再查一次數(shù)據(jù)庫了。
非也,在上面介紹B+ tree時(shí)有提到過,葉子節(jié)點(diǎn)不會(huì)直接存儲(chǔ)數(shù)據(jù)的位置,而是存儲(chǔ)了聚簇索引(clustered index)的值,再通過聚簇索引,找到數(shù)據(jù)對(duì)應(yīng)的位置。
那什么是聚簇索引呢?
Every InnoDB table has a special index called the clustered index where the data for the rows is stored.
簡(jiǎn)單說,聚簇索引就是用來存儲(chǔ)行數(shù)據(jù)的位置的。
什么樣的字段才可以作為聚簇索引?
那當(dāng)然是要具有唯一性的字段,比如:
- 主鍵
- 唯一索引(unique index)所在字段
這兩個(gè)都沒有?沒關(guān)系,mysql會(huì)給你建一個(gè)rowid字段,用它作為聚簇索引:
If the table has no PRIMARY KEY or suitable UNIQUE index, InnoDB internally generates a hidden clustered index named GEN_CLUST_INDEX on a synthetic column containing row ID values.
除了聚簇索引,mysql中的其他索引,都叫二級(jí)索引(secondary index),有時(shí)也翻譯為“輔助索引”。
All indexes other than the clustered index are known as secondary indexes.
回到本小節(jié)開頭的問題,雖然id不在復(fù)合索引里頭,但是mysql里所有的二級(jí)索引的葉子節(jié)點(diǎn),都會(huì)存儲(chǔ)聚簇索引的信息,而id是主鍵,所以所有的葉子節(jié)點(diǎn),都會(huì)有id的信息,因此還是可以走覆蓋索引。
總結(jié)
這篇文章從一顆簡(jiǎn)單的B+樹,引申出了Mysql中常見的幾個(gè)索引概念:
- 單索引(Column Indexes):當(dāng)你為一個(gè)字段建了索引時(shí),mysql默默種了一棵樹。通過這顆樹,可以實(shí)現(xiàn)高效的逐級(jí)查找。
- 復(fù)合索引(Multiple-Column Indexes/Compound Indexes):跟單索引原理一致,比較的方式變了一下,從字符串比較變?yōu)閷?duì)象比較。
- 最左前綴匹配:一個(gè)理所當(dāng)然的概念,只要你理解了上面兩位。
- 覆蓋索引:有些信息已經(jīng)在樹里面了,就不必再麻煩磁盤老人家了。
- 聚簇索引和二級(jí)索引:葉子節(jié)點(diǎn)不直接存儲(chǔ)數(shù)據(jù)位置的信息,存儲(chǔ)數(shù)據(jù)位置信息的,只有聚簇索引。
之所以寫這篇文章,是因?yàn)樯蟼€(gè)星期組內(nèi)分享時(shí),大佬講了一些關(guān)于Mysql執(zhí)行優(yōu)化的東西,比較高深,一下子發(fā)現(xiàn)了自己還有那么多知識(shí)盲點(diǎn),于是惡補(bǔ)了一下Mysql。
這篇文章只是稍微對(duì)Mysql基于B+樹的索引,做了稍微的延伸,還有很多好玩的沒提及,比如:
- 索引如何加速排序
- Mysql的ICP(Index Condition Pushdown Optimization)
- 索引的存儲(chǔ)和緩存
- 索引區(qū)分度和索引長(zhǎng)度
- ...
后面再一塊討論。
參考
- 一本關(guān)于數(shù)據(jù)庫系統(tǒng)理論的經(jīng)典書籍:《數(shù)據(jù)庫系統(tǒng)概念》
- Mysql Index概覽:How MySQL Uses Indexes
- Mysql詞條-覆蓋索引:covering index
- 聚簇索引和二級(jí)索引:Clustered and Secondary Indexes
- StackOverflow上一個(gè)有趣的問題:Mysql covering vs composite vs column index