Innodb索引結構和索引優化方案

數據庫可能存在千萬級的數據,必須將這些行數據以一定的結構組織起來做到高效的增刪改查。下面將分別探索innodb和myisam兩種引擎的索引方案。

 

 

一、InnoDB的索引

1、假設表初始沒有記錄,只有一個空頁,所有記錄按照主鍵順序放到頁中。隨着記錄的增長,一個頁放不下所有記錄,因此會分裂成多個頁,每個頁用雙向指針鏈接,頁與頁之間形成一條雙向鏈表。

這些頁我們稱爲用戶記錄頁,頁內記錄的字段是用戶定義的字段內容。如下圖所示,這是一個以 (id, key1, key2) 組成的聯合索引,頁10的第1條記錄表示(id=1, key1=4, key2='u')。

 

如果Innodb表數據是按這種結構存儲,那麼我們只能做到“頁間遍歷,頁內二分搜索的方式查找”。也就是說當我要查找一條id=30的數據,需要先對頁10進行二分查找,沒找到再從頁28進行二分查找,沒找到再從頁9進行二分查找直到找到,效率很低。爲了提高查找效率,可以爲這些 用戶記錄頁 建立 目錄頁。

 

 

2、目錄頁在頁結構上 和 用戶記錄頁 一樣,目錄頁的行記錄也是以堆的形式存儲,我們稱目錄頁的行記錄爲目錄項,每條記錄只有兩個列(沒有其他的隱藏列),分別是下層頁中最小記錄的主鍵 和 用戶記錄頁的頁號。根據主鍵值(或者索引值)查找數據時,會從最頂層的目錄頁開始檢索,找到目錄頁中符合的記錄後,再根據記錄中的最小記錄的主鍵 和 用戶記錄頁的頁號這兩個信息,找到其對應的用戶頁的最小記錄在磁盤中的位置。

 

記錄頭信息的record_type = 0 表示該記錄是用戶記錄,record_type = 1 是目錄項記錄。

 

需要注意:

目錄頁的頁內查找也是通過上節中介紹的頁目錄(Page Directory)進行二分查找進行檢索的。

目錄頁內的行記錄過多,一個頁放不下的時候,目錄頁也會發生頁分裂,而且目錄頁之間也有雙向鏈接。

 

如下圖所示

 

 

3、如果目錄頁也有多個,那麼查目錄也要遵循目錄頁間遍歷查找,目錄頁內二分查找,爲了避免目錄頁間的橫向遍歷,可以建立多層目錄,頂層目錄只有一個目錄頁。所有目錄頁和用戶記錄頁形成一棵B+樹:

 

 

這麼一來,就可以把 頁間查找 的次數壓縮爲樹的層數,這意味着磁盤IO的次數被壓縮到樹的層數。一般而言,MySQL中一棵B+樹不會超過4層。

Innodb規定,無論是目錄頁還是用戶記錄頁,一個頁至少容納2條記錄,這樣做是爲了控制樹的高度,不讓B+樹的性能優勢喪失。

 

 

主鍵和唯一索引的區別

其實,兩者肯定不同: 主鍵是一種約束,唯一索引是一種索引,兩者在本質上就是不同的。

1、一張表裏只能有一個主鍵約束,可以有多個唯一約束

2、主鍵不能爲空,而唯一可以爲空

3、主鍵列,默認是聚集索引,聚集索引 查詢效率最高

 

主鍵索引(聚簇索引)和普通索引(輔助索引)的區別

什麼是聚集索引(主鍵索引)?

首先 innodb 引擎默認在主鍵上建立聚集索引,通常說的主鍵索引就是聚集索引,聚集索引會保存行上的所有數據,因此不需要額外的 IO

什麼是輔助索引(普通索引)?

輔助索引 (Secondary Index) , 葉子節點只保存了行的鍵值和指向對應行的 "書籤" , 一般指向的是聚集索引,此外 innodb 實現了覆蓋索引 (Covering index) , 即葉子節點除了保存該行的鍵值還保存了對應索引列的值,如果不需要額外數據的話則不需要另外對聚集索引中的數據進行 IO

注: SQL Server的主鍵索引和普通索引的區別僅僅是唯一非空,而mysql innodb下不是, 另外嚴格來說主鍵是約束

爲什麼性別列和其他低選擇性的列不適合加索引?

因爲你訪問索引需要付出額外的 IO 開銷,你從索引中拿到的只是地址,要想真正訪問到數據還是要對錶進行一次 IO。假如你要從表的 100 萬行數據中取幾個數據,那麼利用索引迅速定位,訪問索引的這 IO 開銷就非常值了。但如果你是從 100 萬行數據中取 50 萬行數據,就比如性別字段,那你相對需要訪問 50 萬次索引,再訪問 50 萬次表,加起來的開銷並不會比直接對錶進行一次完整掃描小。

當然凡事不是絕對,如果把性別字段設爲表的聚集索引,那麼就肯定能加快大約一半該字段的查詢速度了。聚集索引指的是表本身中數據按哪個字段的值來進行排序。因此,聚集索引只能有一個,而且使用聚集索引不會付出額外 IO 開銷。當然你得能捨得把聚集索引這麼寶貴資源用到性別字段上。

優化器爲什麼會選擇覆蓋索引用於 count () 等統計問題?

因爲覆蓋索引遠小於聚集索引,可以減少磁盤 IO 操作

 

對於主鍵索引B+樹而言,頁內記錄是按照主鍵大小排序的,同一層的雙向鏈表目錄頁也是按照主鍵索引排序的。

 

對於二級索引B+樹而言,假設該B+樹是以c2字段爲索引。那麼

二級索引的葉子節點頁的記錄只有兩個列:c2列 和 主鍵值,不含其它列和隱藏列。(重點來了)而二級索引的非葉子節點頁內的記錄有3個列:c2列 、 下層頁號 和 主鍵值。

頁內記錄是按照 c2列和主鍵值 (先按c2列排序後按主鍵列排序)的大小排序的,頁目錄(Page Directory)每個槽(Slot)也是分組中擁有最大c2列值的記錄的地址,目錄頁之間也是按照c2列和主鍵大小排序的。

 

爲什麼二級索引的目錄頁和葉子頁的記錄需要保存主鍵索引並且在二級索引值相同的情況下按主鍵索引排序呢?考慮一個問題,如下圖所示:c1是主鍵,c2是普通索引。

 

此時 c2 字段索引的B+樹如下:

 

此時,如果我想插入一個 c1:9、c2:1、c3:'c' 的記錄,在目錄頁3沒有存儲主鍵值的情況下,由於目錄頁3的c2全都是1,所以頁3無法決定該記錄應該插在頁4還是頁5。

但如果目錄頁加上主鍵字段就可以解決這個問題:

 

所以,爲 c2 列建立普通索引 相當於爲 c2 和 主鍵列建 立了一個聯合索引。

對於唯一二級索引來說,也可能會出現 多條記錄鍵值相同的情況(例如NULL,以及MVCC),所以唯一二級索引的目錄項記錄也會包含記錄的主鍵值。

我們結合下面這個例子,看看innodb是如何在使用索引找到對應記錄的,其中key1是普通索引。

SELECT * FROM single_table WHERE key1 = ' a ' AND id > 9000 ;

 

這個sql的行爲如下:

1、在從key1字段的B+樹最頂層目錄頁的記錄查找,找到符合 key1 = 'a' 的記錄後,根據下層頁號找到下一層目錄頁,依此類推,最終找到符合 key1 = 'a'的葉子節點的第一條記錄;

2、沿着葉子節點頁的向後指針找到所有滿足 key1 = 'a'  的記錄,在這個過程中同時一條條的比對 id > 9000 這個條件,得到滿足條件的葉子節點記錄。

也就是說,id > 9000 的比較是在二級索引發生的,而不是在主鍵索引發生的,這樣就可以減少多次回錶帶來的磁盤IO。

3、根據滿足條件的葉子節點記錄中的主鍵值,到主鍵索引B+樹中查找,查找方式和前兩個步驟一致。最終找到主鍵B+樹葉子節點對應的目標記錄。

 

主鍵索引和二級索引不同的一點在於,主鍵索引的葉子頁的記錄包含用戶定義的所有字段,而二級索引的葉子頁的記錄只包含二級索引值和主鍵值。

 

聯合索引的頁

對於聯合索引B+樹(假設聯合索引的字段是 c3,c4),葉子節點頁的記錄會包含 c3、c4 和 主鍵 這3個列。

 

 

二、MyISAM的索引方案

在介紹MyISAM的索引結構之前,需要先介紹MyISAM的行格式,分爲3種:定長記錄(static)、變長記錄(Dynamic)和壓縮格式(compressed)。

MyISAM的索引和表記錄是分成兩個文件存儲的(MYD 和 MYI)。InnoDB 和 MyISAM 都是用B+樹作爲索引結構。不同點在於:

1、InnoDB的表記錄(包含用戶定義的所有字段)是保存在主鍵的葉子頁並通過一系列頁內的數據結構維護(例如以數組爲結構的頁目錄)。

而MyISAM的表記錄是按照記錄的插入順序(而不是主鍵順序)緊密的存儲在MYD文件中,並沒有將它們分成多個頁。

 

如下圖所示

 

 

圖中是定長格式(static)的記錄在 MYD 的存儲方式,每一行擁有一個虛擬的邏輯行號。

 

2、MyISAM的B+樹的葉子節點只存儲索引字段的值 和 行號,InnoDB的B+樹葉子節點存的是行的所有字段值。

MyISAM下,若要根據一個索引值查找一條或多條記錄,需要先在MYI文件的B+數中,根據索引值找到行號,再根據行號到MYD文件找記錄。

所以MyISAM的主鍵索引比InnoDB多了一次回表。

 

MyISAM的主鍵索引是一個二級索引而不是聚集索引。MyISAM的二級索引比InnoDB的二級索引快,因爲前者直接根據行的地址到MYD文件回表,後者是根據主鍵值回表,這意味着要根據主鍵值逐層查到用戶記錄頁的地址和行的頁內地址。

在聚集索引頁不在緩衝區的情況下InnoDB比MyISAM多了2~3次磁盤IO才能獲得行內容。

另外,如果MyISAM使用變長格式的記錄,那麼MyISAM的葉子節點存儲 索引字段值 和 行所在MYD的地址

 

三、索引的代價

空間上,每建立一個索引就會創建一個B+樹,一個索引頁就是16K;

時間上,增刪改需要對所有B+樹操作,由於新數據添加時發生在葉子節點,因此要從根節點找到葉子節點,會涉及多次磁盤IO,B+樹越多,IO次數越多。

此外DML操作可能引起頁分裂、頁回收,InnoDB需要額外的時間完成這些操作以維護節點和記錄的排序。

最後,如果一條查詢語句涉及多個索引,生成執行計劃需要計算使用不同索引執行查詢的成本,並選擇成本最低的哪個索引執行(因爲一般情況下一個查詢語句最多使用一個二級索引)。建立太多索引會導致成本分析時間過長。

 

四、索引條件下推

在 mysql 5.6以後,索引條件下推功能默認是開啓的。

所謂的索引條件下推是指在二級索引中就儘可能根據sql條件減少在二級索引匹配到的記錄數,這是一種用二級索引進行範圍搜索的優化,可以有效減少回表的次數。

 

舉個例子:

有一個聯合索引 key (a, b, c),有一個sql語句:

SELECT * from t WHERE a = ' a'  AND c = 'c';

 

假設 滿足 a = 'a' 的記錄有 10 條,滿足 a = 'a' and c = 'c' 的記錄有5條,聚集索引 M 和聯合二級索引 N 都只有3層,不考慮頁緩存,請問回表次數和磁盤IO次數是多少?

 

查找過程如下:

1、從索引N的根節點(根目錄頁)開始,找到 a < 'a' 且離 a = 'a' 最近的目錄項,進入下一層目錄頁;

2、同上操作,進入到第三層頁,即葉子節點頁。

3、在第三層葉子節點頁頁內找到 a < 'a' 且離 a = 'a'最近的記錄,順着指針在頁內往下遍歷;在頁外順着頁間指針遍歷;(如果 所有a='a' 的記錄都在二級索引N的一個用戶記錄頁,則第3步需要進行1次IO,如果是分佈在2個用戶記錄頁,則第3步需要2次IO,不會分佈超過2個用戶記錄頁的,因爲滿足 a='a' 的記錄只有10條)。

第3步得到了 滿足 a="a" 的10個主鍵id;

4、對着10個主鍵id進行回表,需要回表10次;

5、每次回表查到了完整的表記錄,查看 c字段是否滿足 c = 'c';

 

總回表次數爲10,總IO次數爲(10*3 + 3~4 = 33~34)。

 

如果使用索引條件下推,那麼在二級索引的葉子節點比對的時候,還會檢查 c = 'c' 的條件,因爲 N 的頁的記錄也包含c字段的值。所以檢索完畢後可以在N的葉子節點層得到 5 個滿足條件的主鍵,所以只會回表5次,發生 15 + 3~4 = 18~19次磁盤IO。

 

 

再例如這2個例子:

SELECT * from t WHERE a > ' a'  AND a like '%mbc';
SELECT * FROM t WHERE a = ' a ' AND id > 9000 ;

 

也同樣會用到索引條件下推優化。

 

 

五、索引的功能

索引主要具有3個功能:高效查找、排序和分組;

 

在沒有用到索引的情況下,只能將數據加載到內存,並在內存中排序和分組(如果中間結果太多還需要將中間結果存到磁盤中),這種情況叫做文件排序(filesort)。使用到文件排序會降低查找性能。

而使用了索引,由於記錄插入B+樹時本來就是按索引列的順序插入,因此索引內的數據是有序的,查詢時無需再在內存中排序或分組。

幾種無法使用索引排序的情況:

1、ASC和DESC混用,即對某個字段升序排序,對另一個字段降序排序;

不過,Mysql 8.0 給出了 Descending Index 的新功能,允許聯合索引的建立可以按某個字段升序,某個字段降序的方式插入。

2、order by 包含非同一個索引的列;

3、order by 包含多個是同一聯合索引的列,但列的順序不對(例如對字段A升序,對字段B降序);

4、where 條件的索引列和排序的索引列不同,因爲你無法保證 where a='xxx' 的情況下的記錄 其排序字段 c 是有序的;

5、order by 對列使用函數;

 

 

六、回表的代價

回表的代價就是當執行對二級索引的範圍查詢時,如果查到的記錄數太多,每條記錄都需要在主鍵索引進行回表,回表一次就會在主鍵索引發生多次隨機IO。

一次sql查詢中,回表的記錄(或者說次數)越少,二級索引的效率越高。

 

例如下面的sql語句:

SELECT * from single_table WHERE key1 > ' a ' AND key1< ' c ' ;

可以選擇使用 key1 索引進行查詢,也可以選擇不使用 key1 索引而是直接全表掃描。

這是由查詢優化器決定的,而決定的標準就是 區間 ('a', 'c') 的範圍有多大,也就是取決於使用二級索引會發生回表的次數所帶來的磁盤IO次數 和 全表臊面發生的磁盤IO次數哪個更多。

 

一般情況下,對於指定了 limit 子句的查詢,優化器傾向於使用二級索引 + 回表的方式;

如果一個order by 使用了二級索引作爲排序列,但是記錄數太多,而且沒有覆蓋索引,那麼mysql很可能使用 全表掃描 + 文件排序的方式也不用二級索引的排序,例如:

SELECT * from single_table order by key1 ;

 

七、如何善用索引

1、只爲用於查詢、排序和分組的列創建索引;

2、爲基數大(即重複值少)的列建立索引(因爲重複值多意味着回表次數多);

3、索引列的類型儘量小(例如int類型,較短的varchar類型,能用int就不用bigint,能用tinyint就不用int),這決定了頁內能容納記錄的個數(一次磁盤IO能將更多的目錄項和記錄加載到內存),也決定了樹高;

這個建議尤其適用在主鍵上,因爲所有二級索引的頁都會存一份記錄的主鍵值。

4、爲列前綴建立索引,例如不要對一個varchar(100)的字段c1建立索引,而是對 c1的前10個字節建立索引;

原因有二:一是爲了讓頁內能容納更多的記錄和目錄項,二是字符串的比較,其複雜度會隨字符串長度增加而增加;

另外需要注意:爲列前綴建立索引,該索引只能用於查找,無法用於排序。例如:

ALTER TABLE single_table ADD INDEX idx_key1(key1(10));

SELECT * from single_table ORDER BY key1 limit 10;

 

這個排序沒有用到key1的索引,因爲B+樹只對key1的前10個字節排序了,沒有給整個key1值排序;

 

5、覆蓋索引(即避免使用*,而是在sql中註明要查詢的列):可以徹底避免回表,對於二級索引而言,覆蓋索引會讓優化器會選擇使用二級索引而不會選擇全表掃描。

6、讓索引列以列名的形式在搜索條件中單獨出現(別使用函數或對列計算)。

7、主鍵的插入儘可能按順序插入:這是爲了避免也分裂。對於二級索引,就沒辦法要求它的插入頁按順序了,這是業務需求決定的。

考慮一種情況:一個頁按照id爲 1、3、5、7、9、2、4、6、8、10的順序插入,這會導致頁分裂;

但是如果 1、2、3、4、5、6,我刪除id=2的記錄,再插入id=2的記錄,是不會導致頁分裂的,他會重新佔用頁內的已刪除空間。

八. 如何查看索引使用的情況:

show status like 'Handler_read%';

注意:

  • handler_read_key:這個值越高越好,越高表示使用索引查詢到的次數。
  • handler_read_rnd_next:這個值越高,說明查詢比較的低效。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章