mysql> create table T (
ID int primary key,
k int NOT NULL DEFAULT 0,
s varchar(16) NOT NULL DEFAULT '',
index k(k))
engine=InnoDB;
insert into T values(100,1, 'aa'),(200,2,'bb'),(300,3,'cc'),(500,5,'ee'),(600,6,'ff'),(700,7,'gg');
執行 select * from T where k between 3 and 5,需要執行幾次樹的搜索操作,會掃描多少行?
這條SQL查詢語句的執行流程:
- 1.在k索引樹上找到k=3的記錄,取得 ID = 300;
- 2.再到ID索引樹查到ID=300對應的R3;
- 3.在k索引樹取下一個值k=5,取得ID=500;
- 4.再回到ID索引樹查到ID=500對應的R4;
- 5.在k索引樹取下一個值k=6,不滿足條件,循環結束。
在這個過程中,回到主鍵索引樹搜索的過程,我們稱為回表。可以看到,這個查詢過程讀了k索引樹的3條記錄(步驟1、3和5),回表了兩次(步驟2和4)。
在這個例子中,由于查詢結果所需要的數據只在主鍵索引上有,所以不得不回表。那么,有沒有可能經過索引優化,避免回表過程呢?
覆蓋索引
如果執行的語句是select ID from T where k between 3 and 5,這時只需要查ID的值,而ID的值已經在k索引樹上了,因此可以直接提供查詢結果,不需要回表。也就是說,在這個查詢里面,索引k已經“覆蓋了”我們的查詢需求,我們稱為覆蓋索引。
由于覆蓋索引可以減少樹的搜索次數,顯著提升查詢性能,所以使用覆蓋索引是一個常用的性能優化手段。
需要注意的是,在引擎內部使用覆蓋索引在索引k上其實讀了三個記錄,R3~R5(對應的索引k上的記錄項),但是對于MySQL的Server層來說,它就是找引擎拿到了兩條記錄,因此MySQL認為掃描行數是2。
基于上面覆蓋索引的說明,我們來討論一個問題:在一個市民信息表上,是否有必要將身份證號和名字建立聯合索引?
假設這個市民表的定義是這樣的:
CREATE TABLE `tuser` (
`id` int(11) NOT NULL,
`id_card` varchar(32) DEFAULT NULL,
`name` varchar(32) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`ismale` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `id_card` (`id_card`),
KEY `name_age` (`name`,`age`)
) ENGINE=InnoDB
我們知道,身份證號是市民的唯一標識。也就是說,如果有根據身份證號查詢市民信息的需求,我們只要在身份證號字段上建立索引就夠了。而再建立一個(身份證號、姓名)的聯合索引,是不是浪費空間?
如果現在有一個高頻請求,要根據市民的身份證號查詢他的姓名,這個聯合索引就有意義了。它可以在這個高頻請求上用到覆蓋索引,不再需要回表查整行記錄,減少語句的執行時間。
當然,索引字段的維護總是有代價的。因此,在建立冗余索引來支持覆蓋索引時就需要權衡考慮了。這正是業務DBA,或者稱為業務數據架構師的工作。
最左前綴原則
看到這里你一定有一個疑問,如果為每一種查詢都設計一個索引,索引是不是太多了。如果我現在要按照市民的身份證號去查他的家庭地址呢?雖然這個查詢需求在業務中出現的概率不高,但總不能讓它走全表掃描吧?反過來說,單獨為一個不頻繁的請求創建一個(身份證號,地址)的索引又感覺有點浪費。應該怎么做呢?
B+樹這種索引結構,可以利用索引的“最左前綴”,來定位記錄。
為了直觀地說明這個概念,我們用(name,age)這個聯合索引來分析。
可以看到,索引項是按照索引定義里面出現的字段順序排序的。
當你的邏輯需求是查到所有名字是“張三”的人時,可以快速定位到ID4,然后向后遍歷得到所有需要的結果。
如果你要查的是所有名字第一個字是“張”的人,你的SQL語句的條件是"where name like ‘張%’"。這時,你也能夠用上這個索引,查找到第一個符合條件的記錄是ID3,然后向后遍歷,直到不滿足條件為止。
可以看到,不只是索引的全部定義,只要滿足最左前綴,就可以利用索引來加速檢索。這個最左前綴可以是聯合索引的最左N個字段,也可以是字符串索引的最左M個字符。
基于上面對最左前綴索引的說明,我們來討論一個問題:在建立聯合索引的時候,如何安排索引內的字段順序。
這里的評估標準是,索引的復用能力。因為可以支持最左前綴,所以當已經有了(a,b)這個聯合索引后,一般就不需要單獨在a上建立索引了。因此,第一原則是,如果通過調整順序,可以少維護一個索引,那么這個順序往往就是需要優先考慮采用的。
所以現在你知道了,這段開頭的問題里,我們要為高頻請求創建(身份證號,姓名)這個聯合索引,并用這個索引支持“根據身份證號查詢地址”的需求。
那么,如果既有聯合查詢,又有基于a、b各自的查詢呢?查詢條件里面只有b的語句,是無法使用(a,b)這個聯合索引的,這時候你不得不維護另外一個索引,也就是說你需要同時維護(a,b)、(b) 這兩個索引。
這時候,我們要考慮的原則就是空間了。比如上面這個市民表的情況,name字段是比age字段大的 ,那我就建議你創建一個(name,age)的聯合索引和一個(age)的單字段索引。
索引下推
以市民表的聯合索引(name, age)為例。如果現在有一個需求:檢索出表中“名字第一個字是張,而且年齡是10歲的所有男孩”。那么,SQL語句是這么寫的:
mysql> select * from tuser where name like '張%' and age=10 and ismale=1;
你已經知道了前綴索引規則,所以這個語句在搜索索引樹的時候,只能用 “張”,找到第一個滿足條件的記錄ID3。當然,這還不錯,總比全表掃描要好。
然后呢?
當然是判斷其他條件是否滿足。
在MySQL 5.6之前,只能從ID3開始一個個回表。到主鍵索引上找出數據行,再對比字段值。
而MySQL 5.6 引入的索引下推優化(index condition pushdown), 可以在索引遍歷過程中,對索引中包含的字段先做判斷,直接過濾掉不滿足條件的記錄,減少回表次數。
-
無索引下推執行流程圖
image.png -
索引下推執行流程
image.png
這兩個圖里面,每一個虛線箭頭表示回表一次。
第一個圖中,在(name,age)索引里面我特意去掉了age的值,這個過程InnoDB并不會去看age的值,只是按順序把“name第一個字是’張’”的記錄一條條取出來回表。因此,需要回表4次。
他們的區別是,InnoDB在(name,age)索引內部就判斷了age是否等于10,對于不等于10的記錄,直接判斷并跳過。在這個例子中,只需要對ID4、ID5這兩條記錄回表取數據判斷,就只需要回表2次。
——學自極客時間