索引是在存儲引擎層實現的,且在 MySQL
不同存儲引擎中的實現也不同,本篇文章介紹的是 MySQL
的 InnoDB
的索引。
下文將以這張表爲例開展。
# 創建一個主鍵爲 id 的表,表中有字段 k,並且在 k 上有索引。
create table T(
`id` int(11) AUTO_INCREMENT,
`k` int(11) NOT NULL,
`name` varchar(16),
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY (`k`),
KEY `name_age` (`name`,`age`)
) ENGINE=InnoDB;
# 插入多條數據
insert into T values (100, 'Bob'),(200, 'Peter'),(300,'Mary');
一、InnoDB索引模型
在 InnoDB
中,表都是根據主鍵順序以索引的形式存放的,也就是數據放在主鍵索引上,其他索引上保存的是主鍵 id
,這種存儲方式的表稱爲索引組織表。
InnoDB
使用了 B+樹
索引模型,所以數據都是存儲在 B+樹
中的。每一個索引在 InnoDB
裏面對應一棵 B+樹
。
二、索引的類型
2.1 主鍵約束:主鍵索引和二級索引
主鍵索引的葉子節點存的是整行數據。在 InnoDB
裏,主鍵索引也被稱爲聚簇索引。
非主鍵索引的葉子節點內容是主鍵的值。在 InnoDB
裏,非主鍵索引也被稱爲二級索引、輔助索引。
2.1.1 主鍵索引和非主鍵索引的區別
主鍵查詢方式:只需要搜索 ID
這棵 B+樹
;
普通索引查詢方式:先搜索普通索引的 B+樹
,得到主鍵索引 ID
的值,再到 ID
索引樹上搜索,這個過程稱爲回表。
2.1.2 主鍵索引的選取規則
從空間的角度出發:主鍵列長度儘可能短,每個二級索引的葉子節點是主鍵,主鍵過長會導致二級索引佔用空間更大。
從性能的角度出發:推薦使用自增索引,非自增主鍵在插入和刪除的操作中,會導致頁分裂和頁合併。
2.1.3 非主鍵索引的優化:覆蓋索引
先看下面這個 sql
:
select * from T where k = 100;
這個 sql
語句會在 k
索引樹上找到 k=100
的記錄,取得 ID=15
;
再到 ID
索引樹查到 ID=15
對應的記錄,發生了回表,如果將 sql
語句改爲
select id from T where k = 100;
因爲 ID
的值已經在 k
索引樹上了,因此可以直接提供查詢結果,不需要回表。
如果一個索引包含(或覆蓋)所有需要查詢的字段的值,稱爲覆蓋索引,即只需掃描索引而無須回表。
2.1.4 業務字段做主鍵的條件
如果不使用自增 ID
做主鍵,用業務字段直接做主鍵,則需要滿足:只有一個索引,且該索引爲唯一索引。
由於沒有其他索引,所以不用考慮其他索引的葉子節點大小的問題,把這個索引設置爲主鍵,避免每次查詢需要搜索兩棵樹。
2.1.5 索引的重建
- 主鍵索引的重建
# 正確做法
alter table T engine=InnoDB
# 錯誤做法
alter table T drop primary key;
alter table T add primary key(id);
直接刪掉主鍵索引會使得所有的二級索引都失效,並且會用 ROWID
來作主鍵索引。
- 非主鍵索引的重建
alter table T drop index k;
alter table T add index(k);
索引可能因爲刪除,或者頁分裂等原因,導致數據頁有空洞,重建索引的過程會創建一個新的索引,把數據按順序插入,這樣頁面的利用率最高,達到省空間的目的。
2.2 索引字段數量:聯合索引和單列索引
在上面的建表語句中,可以看到有兩個索引,一個是 k
索引,一個是name_age
索引,不難看出,前者是單列索引,而後者就是聯合索引了。
爲什麼會有聯合索引呢?當查詢條件爲2個及以上時,比如當經常要用 name
和 age
去查詢數據時:
select * from T where name = 'Job' and age = 28;
創建一個 (name,age)
的聯合索引,相當於創建了 name
和 name、age
這兩個組合的索引,可以加速檢索。
2.2.1 最左前綴原則
顧名思義是最左優先,以最左邊的爲起點任何連續的索引都能匹配上。
聯合索引的示例圖如下:
索引項是按照索引定義裏的字段順序來排序的,因此在創建聯合索引時,要根據業務需求,where子句中使用最頻繁的一列放在最左邊。
當已經有了 (a,b) 這個聯合索引後,一般就不需要單獨在 a 上建立索引了。因此,第一原則是,如果通過調整順序,可以少維護一個索引,那麼這個順序往往就是需要優先考慮採用的。
當創建 (a,b,c)
聯合索引時,相當於創建了 (a)
單列索引、(a,b)
聯合索引以及 (a,b,c)
聯合索引。
想要索引生效的話,只能使用 a
和 a,b
和 a,b,c
三種組合;a,c
組合也可以,但實際上只用到了 a
的索引,並沒有用到 c
。
2.2.2 索引下推
MySQL 5.6 引入索引下推優化(index condition pushdown),可以在索引遍歷過程中,對索引中包含的字段先做判斷,直接過濾掉不滿足條件的記錄,減少回表次數。
比如根據(name,age)
聯合索引查詢所有滿足名稱以“張”開頭的索引,然後直接再篩選出年齡小於等於10的索引,之後再回表查詢全行數據。
注意:innodb
引擎的表,索引下推只能用於二級索引。
2.3 唯一約束:唯一索引和普通索引
普通索引允許被索引的數據列包含重複的值,創建唯一索引的目的一般不是爲了提高訪問速度,而只是爲了避免數據重複。
2.3.1 change buffer機制
當需要更新一個數據頁時,如果數據頁在內存中就直接更新,而如果這個數據頁還沒有在內存中的話,在不影響數據一致性的前提下,InnoDB 會將這些更新操作緩存在 change buffer 中,這樣就不需要從磁盤中讀入這個數據頁了。在下次查詢需要訪問這個數據頁的時候,將數據頁讀入內存,然後執行 change buffer 中與這個頁有關的操作。通過這種方式就能保證這個數據邏輯的正確性。
2.3.2 唯一索引和普通索引的選擇
不推薦使用唯一索引,這是因爲:
從查詢的角度出發:
- 如果查詢結果全在內存上:唯一索引在數據頁中查找滿足查詢條件的第一條記錄即可返回;普通索引需要再獲取下一條記錄,由於索引項是有序的且內存操作,多一次判斷的時間損耗可忽略不計;
- 如果查詢結果不在內存上:先把數據頁加載到內存中,再按照查詢結果全在內存的流程處理。
從更新的角度出發:
- 如果需要更新的記錄全在內存上,直接更新內存記錄並返回;
- 如果需要更新的記錄不在內存上以及部分在內存上:唯一索引需要先將需要更新的記錄從磁盤中加載到內存,更新內存記錄並寫
redolog
;普通索引將更新操作寫入change buffer
,通知執行器更新完成;在下次讀相關記錄的時候,先把原記錄讀取到內存,再將change buffer
上的操作在內存記錄上回放,並寫redolog
; - 普通索引在更新時,節省了更新時從磁盤讀取記錄的時間,而唯一索引在更新時,若記錄不在內存,需要從磁盤讀取記錄到內存。
結論:change buffer 只適用於普通索引,而不適用於唯一索引。
後記
關於索引的知識點比較多,但每看一遍專欄都會有新的收穫。fighting!💪