Mysql InnoDB索引分析
索引介紹
在 MySQL 中,索引是在存儲引擎層實現的,所以并沒有統一的索引標準,即不同存儲引擎的索引的工作方式并不一樣。而即使多個存儲引擎支持同一種類型的索引,其底層的實現也可能不同。InnoDB 存儲引擎在 MySQL 數據庫中使用最為廣泛,下面我們介紹一下InnoDB的索引模型。
在 InnoDB 中,表都是根據主鍵順序以索引的形式存放的,這種存儲方式的表稱為索引組織表。InnoDB 使用了 B+ 樹索引模型,所以數據都是存儲在 B+ 樹中的。每一個索引在 InnoDB 里面對應一棵 B+ 樹。
B+樹的介紹可以看以下文章:
Mysql的InnoDB索引原理詳解
索引維護
假設,我們有一個主鍵列為 ID 的表,表中有字段 k,并且在 k 上有索引。這個表的建表語句是:
mysql> create table T( id int primary key, k int not null, name varchar(16), index (k))engine=InnoDB;表中 R1~R5 的 (ID,k) 值分別為 (100,1)、(200,2)、(300,3)、(500,5) 和 (600,6),兩棵樹的示例示意圖如下。
?
從圖中不難看出,根據葉子節點的內容,索引類型分為主鍵索引和非主鍵索引。
主鍵索引的葉子節點存的是整行數據。在 InnoDB 里,主鍵索引也被稱為聚簇索引(clustered index)。
非主鍵索引的葉子節點內容是主鍵的值。在 InnoDB 里,非主鍵索引也被稱為二級索引(secondary index)。
B+ 樹為了維護索引有序性,在插入新值的時候需要做必要的維護。以上面這個圖為例,如果插入新的行 ID 值為 700,則只需要在 R5 的記錄后面插入一個新記錄。如果新插入的 ID 值為 400,就相對麻煩了,需要邏輯上挪動后面的數據,空出位置。
而更糟的情況是,如果 R5 所在的數據頁已經滿了,根據 B+ 樹的算法,這時候需要申請一個新的數據頁,然后挪動部分數據過去。這個過程稱為頁分裂。在這種情況下,性能自然會受影響。
除了性能外,頁分裂操作還影響數據頁的利用率。原本放在一個頁的數據,現在分到兩個頁中,整體空間利用率降低大約 50%。
基于上面的索引維護過程說明,我們來討論一個案例:
你可能在一些建表規范里面見到過類似的描述,要求建表語句里一定要有自增主鍵。當然事無絕對,我們來分析一下哪些場景下應該使用自增主鍵,而哪些場景下不應該。
自增主鍵是指自增列上定義的主鍵,在建表語句中一般是這么定義的: NOT NULL PRIMARY KEY AUTO_INCREMENT。插入新記錄的時候可以不指定 ID 的值,系統會獲取當前 ID 最大值加 1 作為下一條記錄的 ID 值。
也就是說,自增主鍵的插入數據模式,正符合了我們前面提到的遞增插入的場景。每次插入一條新記錄,都是追加操作,都不涉及到挪動其他記錄,也不會觸發葉子節點的分裂。
除了考慮性能外,我們還可以從存儲空間的角度來看。假設你的表中確實有一個唯一字段,比如字符串類型的身份證號,那應該用身份證號做主鍵,還是用自增字段做主鍵呢?
由于每個非主鍵索引的葉子節點上都是主鍵的值。如果用身份證號做主鍵,那么每個二級索引的葉子節點占用約 20 個字節,而如果用整型做主鍵,則只要 4 個字節,如果是長整型(bigint)則是 8 個字節。
顯然,主鍵長度越小,普通索引的葉子節點就越小,普通索引占用的空間也就越小。
所以,從性能和存儲空間方面考量,自增主鍵往往是更合理的選擇。
有沒有什么場景適合用業務字段直接做主鍵的呢?還是有的。比如,有些業務的場景需求是這樣的:
只有一個索引;
該索引必須是唯一索引。
由于沒有其他索引,所以也就不用考慮其他索引的葉子節點大小的問題。這時候我們就要優先考慮上一段提到的“盡量使用主鍵查詢”原則,直接將這個索引設置為主鍵,可以避免每次查詢需要搜索兩棵樹。
索引覆蓋
還是上面這張表,如果我執行select * from T where k between 3 and 5,需要執行幾次樹的搜索操作,會掃描多少行?
現在,我們一起來看看這條 SQL 查詢語句的執行流程:
在 k 索引樹上找到 k=3 的記錄,取得 ID = 300;
再到 ID 索引樹查到 ID=300 對應的 R3;
在 k 索引樹取下一個值 k=5,取得 ID=500;
再回到 ID 索引樹查到 ID=500 對應的 R4;
在 k 索引樹取下一個值 k=6,不滿足條件,循環結束。
這里我們查找的是3-5之間的數,為什么還要查找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。
下面是這兩條語句的分析:
1)EXPLAIN select * from T where k between 3 and 5
?
能夠命中索引,但是name字段必須通過回表查詢得到>>>Using index condition
2)EXPLAIN select id from T where k between 3 and 5
?
能夠命中索引,且索引中本來就存儲的id,所以不需要回表>>>Using index
總結
以上是生活随笔為你收集整理的Mysql InnoDB索引分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 白话解析:一致性哈希算法 consist
- 下一篇: mysql不同count的性能分析