05 | 深刻淺出索引(下)

在上一篇文章中,我和你介紹了InnoDB索引的數據結構模型,今天咱們再繼續聊聊跟MySQL索引有關的概念。mysql

在開始這篇文章以前,咱們先來看一下這個問題:sql

在下面這個表T中,若是我執行 select * from T where k between 3 and 5,須要執行幾回樹的搜索操做,會掃描多少行?數據庫

下面是這個表的初始化語句。性能優化

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');

圖1 InnoDB的索引組織結構

如今,咱們一塊兒來看看這條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,不知足條件,循環結束。spa

在這個過程當中,回到主鍵索引樹搜索的過程,咱們稱爲回表。能夠看到,這個查詢過程讀了k索引樹的3條記錄(步驟一、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。

備註:關於如何查看掃描行數的問題,我將會在第16文章《如何正確地顯示隨機消息?》中,和你詳細討論。

基於上面覆蓋索引的說明,咱們來討論一個問題:在一個市民信息表上,是否有必要將身份證號和名字創建聯合索引?

假設這個市民表的定義是這樣的:

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)這個聯合索引來分析。

圖2 (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), 能夠在索引遍歷過程當中,對索引中包含的字段先作判斷,直接過濾掉不知足條件的記錄,減小回表次數。

圖3和圖4,是這兩個過程的執行流程圖。

圖3 無索引下推執行流程

圖4 索引下推執行流程

在圖3和4這兩個圖裏面,每個虛線箭頭表示回表一次。

圖3中,在(name,age)索引裏面我特地去掉了age的值,這個過程InnoDB並不會去看age的值,只是按順序把「name第一個字是’張’」的記錄一條條取出來回表。所以,須要回表4次。

圖4跟圖3的區別是,InnoDB在(name,age)索引內部就判斷了age是否等於10,對於不等於10的記錄,直接判斷並跳過。在咱們的這個例子中,只須要對ID四、ID5這兩條記錄回表取數據判斷,就只須要回表2次。

小結

今天這篇文章,我和你繼續討論了數據庫索引的概念,包括了覆蓋索引、前綴索引、索引下推。你能夠看到,在知足語句需求的狀況下, 儘可能少地訪問資源是數據庫設計的重要原則之一。咱們在使用數據庫的時候,尤爲是在設計表結構時,也要以減小資源消耗做爲目標。

接下來我給你留下一個問題吧。

實際上主鍵索引也是可使用多個字段的。DBA小呂在入職新公司的時候,就發現本身接手維護的庫裏面,有這麼一個表,表結構定義相似這樣的:

CREATE TABLE `geek` (
`a` int(11) NOT NULL,
`b` int(11) NOT NULL,
`c` int(11) NOT NULL,
`d` int(11) NOT NULL,
PRIMARY KEY (`a`,`b`),
KEY `c` (`c`),
KEY `ca` (`c`,`a`),
KEY `cb` (`c`,`b`)
) ENGINE=InnoDB;

公司的同事告訴他說,因爲歷史緣由,這個表須要a、b作聯合主鍵,這個小呂理解了。

可是,學過本章內容的小呂又納悶了,既然主鍵包含了a、b這兩個字段,那意味着單獨在字段c上建立一個索引,就已經包含了三個字段了呀,爲何要建立「ca」「cb」這兩個索引?

同事告訴他,是由於他們的業務裏面有這樣的兩種語句:

select * from geek where c=N order by a limit 1;
select * from geek where c=N order by b limit 1;

我給你的問題是,這位同事的解釋對嗎,爲了這兩個查詢模式,這兩個索引是否都是必須的?爲何呢?

你能夠把你的思考和觀點寫在留言區裏,我會在下一篇文章的末尾和你討論這個問題。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。

上期問題時間

上期的問題是,經過兩個alter 語句重建索引k,以及經過兩個alter語句重建主鍵索引是否合理。

在評論區,有同窗問到爲何要重建索引。咱們文章裏面有提到,索引可能由於刪除,或者頁分裂等緣由,致使數據頁有空洞,重建索引的過程會建立一個新的索引,把數據按順序插入,這樣頁面的利用率最高,也就是索引更緊湊、更省空間。

這道題目,我給你的「參考答案」是:

重建索引k的作法是合理的,能夠達到省空間的目的。可是,重建主鍵的過程不合理。不管是刪除主鍵仍是建立主鍵,都會將整個表重建。因此連着執行這兩個語句的話,第一個語句就白作了。這兩個語句,你能夠用這個語句代替 : alter table T engine=InnoDB。在專欄的第12篇文章《爲何表數據刪掉一半,表文件大小不變?》中,我會和你分析這條語句的執行流程。

相關文章
相關標籤/搜索