閱讀該部分內容時,需要提前了解B+Tree樹基本知識點,否則可能有些內容你并不能很好的體會到。對于下面幾點內容如果不是很清楚,可以閱讀我之前寫的Mysql簡敘一文中的內容進行了解。
- 每個索引都是一顆B+Tree,最下面的一層是葉子節點,其余都是內節點。而葉子節點中存儲的都是用戶相關數據,而內節點存儲的是頁節點相關信息。
- InnoDB會自動為主鍵(沒有會自動添加)建立聚族索引,聚族索引包含了所有的用戶數據。
- 二級索引中的用戶數據是通過索引列和主鍵組成的,利用二級索引找到對應的記錄時,如果需要獲取整條記錄信息需要通過二級索引中的主鍵通過回表的方式從聚族索引中獲取。
- B+Tree中每層節點都是按照索引列從小到大的順序排列的。
- 查找數據時是從B+Tree的根節點開始往下找。
索引使用示例
對于后面的示例,我們定義如下表結構:
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用戶ID',
`user_name` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '用戶姓名',
`age` int(11) NOT NULL COMMENT '年齡',
`phone_no` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '手機號',
PRIMARY KEY (`id`),
KEY `index_user_age` (`user_name`,`age`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
從表結構中我們可以知道,在這個表中我們存在兩個索引,一個是根據主鍵和用戶數據創建的聚族索引,另外一個則是user_name和age組成的二級索引,所以在這個表中存在兩顆B+Tree樹。
全值匹配
SELECT * FROM t_user WHERE user_name = 'tom' AND age = 18;
我們建立的index_user_age索引兩個字段都在最后的查詢條件中,這個查詢的執行過程如下:
- 首先通過index_user_age這個B+Tree樹能很快的找到對應的記錄。
- 因為需要返回所有數據,而index_user_age該索引樹中只有user_name,age,id信息,所以需要通過回表的方式從聚族索引找到對應的整條用戶數據然后返回。
可能你一直有一個疑問就是我們的查詢語句中條件的順序是user_namge和age,這個順序和索引中字段的順序一樣能用到索引。但是如果我的查詢語句中條件的順序是age和user_name時,還能用到索引嗎?答案是能用到索引。在Mysql中存在查詢優化引擎,它會根據你創建的索引里的字段順序去優化你的SQL,所以對于這一點完全不用擔心。
從上面的分析結果可以看出它們的結果是一樣的,所以你是不用擔心查詢條件中的字段順序的。
最左匹配原則
SELECT * FROM t_user WHERE age =18;
上面的SQL能用到索引嗎?如果你了解B+Tree索引知識,你就能知道是不行的。因為它不符合最左匹配原則,那為什么會這樣子呢?我們先看看index_user_age索引具體是什么樣子的。
通過上圖我想你很清楚索引的結構了。最后一層就是我們的索引數據,它們的順序是按照user_name->age排列的,我們在使用index_user_age索引時,我們先只能先按照user_name按順序找到所有的對應的索引數據,然后再根據age去做進一步的篩選,所以對于上面的SQL語句我們無法用到index_user_age索引。那下面這個語句可以用到index_user_age索引嗎?
SELECT * FROM t_user WHERE user_name = 'tom';
相信你心中已經有了答案。我們直接看這個語句的分析結果,看看是不是和我們說的一致。
從explain結果可以看出,第一條SQL是無法用到任何索引的,但是第二條可以用到index_user_age索引。
索引下推
我們修改index_user_age索引,在原先index_user_age的基礎上增加phone_no索引字段,創建新的索引index_user_age_phone索引。修改后的表結構如下:
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用戶ID',
`user_name` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '用戶姓名',
`age` int(11) NOT NULL COMMENT '年齡',
`phone_no` varchar(255) COLLATE utf8mb4_bin NOT NULL COMMENT '手機號',
`address` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_user_age_phone` (`user_name`,`age`,`phone_no`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
現在我有一個查詢如下:
SELECT * FROM t_user WHERE user_name = 'jack' AND phone_no = '11111111111';
我現在想問的是這個查詢中能用到索引嗎?phone_no這個能用到索引嗎?根據前面的最左匹配原則可以知道這個查詢能用到索引index_user_age_phone,好像phone_no這個是沒法用到索引的。對于index_user_age_phone索引來說,它內部索引列的字段順序是按照user_name->age->phone_no按順序排列的,對于我們的查詢條件,搜索時我們只能用到user_name這個列了。但是實際上phone_no這個列也能用到,只不過不是用在搜索查詢中。
通過二級索引查詢記錄時,我們只能獲取到索引列上的值和主鍵,如果我們的返回結果中需要返回該記錄的所有字段信息,我們就不得不通過回表的方式從聚族索引再次獲取該記錄的完整信息。這個過程如下所示:
首先我們根據user_name找到所有滿足記錄數據,然后通過回表的方式從對應的主鍵索引(聚族索引)中找到完整的用戶數據,然后再篩選phone_no這個條件?;乇磉@個操作你可以理解為通過主鍵ID從聚族索引中獲取用戶數據,索引回表是有消耗的,雖然通過主鍵查找記錄很快,但是通過user_name匹配到的數據很多時,還是會影響查詢速度的。那有沒有方式可以減少回表的次數呢?
估計聰明的你肯定想到了,從圖中我們可以看到現在有三條記錄滿足 user_name=jack 這個條件,正常情況下我們需要回表三次。如果我在回表前就篩選phone_no這個查詢條件,這樣是不是就能減少我們的回表次數呢?如果在回表前判斷phone_no這個條件,這時我們只有一條記錄滿足條件,回表次數也從3次變成1次了。
對于上面這種方式,我們就叫做 索引下推 。這個特性是在Mysql5.6及以后才版本才提供,對于之前的Mysql版本是沒有這個優化的。
列前綴匹配
在開發中我們可能會聽別人說like查詢無法使用索引,但是實際真的如此嗎?其實這個跟我們之前說的最左匹配原則很相似,例如有下面的這個查詢:
SELECT * FROM t_user WHERE user_name LIKE 't%';
這個查詢的意思是查找名字以t開頭的用戶,對于這個查詢是能使用到索引的。在這個查詢中會用到index_user_age_phone這個索引,因為在二級索引樹中,索引的內容是按照user_name、age、phone_no這個順序排列的。而我們的查詢是查user_name以字母t開頭的,所以在這個查詢中我們的索引能生效,這個方式我們通常稱作列前綴匹配。但是下面這個SQL索引是不生效的。
SELECT * FROM t_user WHERE user_name LIKE '%t%';
這個SQL的意思是查找名字中包含字母t的用戶,所以這個時候索引不會生效。我們可以通過explain結果來驗證我們的結論。
從上面的explain結果我們可以看出,對于like 't%' 即前綴匹配是可以用到索引的,而后面的 like '%t%' 是無法用到索引的。
索引用于排序
索引不僅僅只是用作查詢條件,同時索引還能作用于排序。例如現在有下面這個排序:
SELECT * FROM t_user ORDER BY user_name,age,phone_no LIMIT 10;
例如上面這個SQL,它是可以用到索引的。因為對于index_user_age_phone這個索引樹,它里面的數據是按照user_name、age和phone_no組合的值從小到大排列的,所以這里是能用到索引的??赡苣銜枺J情況下排序是ASC,跟索引里面一樣是從小到大,但如果換成DESC,即從大到小的順序排列那還能使用索引嗎?答案是可以,只要字段順序沒變,不管是ASC還是DESC它都能用到索引。(注意:如果使用explain查看結果可能發現它沒有使用任何索引,這可能是因為你表中的數據量太少,Mysql掃全表比使用索引更快,因為使用二級索引需要回表)。
那是不是排序都能用上索引呢?答案是不是的。并不是所有的排序都能用上索引,對于下面的幾種情況是無法通過索引來排序的。
- ASC和DESC混用
SELECT * FROM t_user ORDER BY user_name ASC ,age DESC ,phone_no ASC LIMIT 10;
- where子句中出現非排序的索引列
SELECT * FROM t_user WHERE address = 'xxxx' ORDER BY user_name LIMIT 10;
- 排序列包含非統一個索引的列
SELECT * FROM t_user ORDER BY user_name,address;
- 排序列使用了函數
SELECT * FROM t_user ORDER BY UPPER(user_name)
索引用于分組
索引用于分組這個跟排序類似,基本上的原則也都一樣。
回表和索引覆蓋
前面說過,有時候使用explain分析語句時發現并不是按照一些規則來的,至于為什么會如此這個就跟回表有關了。我們在使用二級索引找到對應數據后,如果我們要返回的列不在索引中,這個時候我們就需要進行回表?;乇硭枰ㄟ^ID再去聚族索引中找到原始數據,并且這里面并不是一個順序I/O。如果對于大量的數據需要回表時,Mysql往往有時候不會使用二級索引而是直接掃表。因為大量的回表它的效率有可能還沒有掃全表的效率高。
對于我們查詢而言,如果應該盡量不使用號返回所有字段,而應該只取我們需要的字段返回即可。同時我們還可以通過索引覆蓋*的方式來減少回表。例如我有一個查詢是通過user_name查詢用戶的年齡和手機號,我在創建索引時不僅僅只在user_name列上創建索引,我可以創建一個由user_name、age和phone_no三個字段組成的索引,而查詢的返回值只需要返回這三個字段即可。
建索引的一些建議
對于索引而言,并不是越多越好。索引是可以提高我們的查詢效率,但是如果索引過多也會帶來一些負面影響。每個索引在InnoDB中都是一顆B+Tree,多一個索引就多一顆B+Tree,這會占用過多的磁盤空間。同時在更新和刪除時,需要同時更新和刪除索引中的數據,這樣會導致數據庫的寫入性能受影響。很多情況下面,有些索引是多余的,我們應該精簡我們的索引。通常我們在建索引時應該有下面的幾個原則:
- 只為用戶搜索、排序或分組的列上建立索引。
對于這點我想很好理解,如果有些列我們根本用不上那就沒必要建索引,多建的索引并不能給我們帶來好處反而還浪費了服務器的資源。 - 為基數大的列建立索引。
什么是基數呢?例如性別這個列,正常情況下我們一般也就是存三個值。例如:男、女、未知。即使我們的表里面存了100萬條數據,它的值還是在這三個里面,而這個列的基數就是3。為了加快搜索我們在性別這個列上面創建索引,這種方式并不能帶來查詢性能上的明顯改善,對于這種列我們就沒有必要在這個列上單獨創建索引了。 - 索引列的類型盡量小。
例如對于整數類型在Mysql中就有好幾種,例如tinyint,mediumint,int,bigint。它們所占的空間依次遞增,而能表示的數字大小也同樣依次遞增。如果一個整數列的值能用int表示完整我們就不應該用bigint,數據類型越小,索引所占的空間也就越小,那么在一個業內能存放的數據也就越多。這樣就能減少磁盤I/O的次數,減少磁盤I/O的次數也就意味著性能的提升。 - 刪除冗余和重復索引
什么樣的索引算是冗余和重復索引呢?例如我創建了一個以user_name列和age列組合而成的索引index_user_age,然后又創建了一個以user_name列的索引index_user,這種情況下索引就重復了。對于這種情況,我們可以刪掉index_user索引,因為index_user_age索引完全可以替代index_user索引的效果,所以我們沒必要多加一個索引浪費服務器資源。