談到 MYSQL 索引服務端的同學應該是熟悉的不能再熟悉,新建表的時候怎么着都知道先來個主鍵索引,對於經常查詢的列也會加個索引加快查詢速度。那么 MYSQL 索引都有哪些類型呢?索引結構是什么樣的呢?有了索引是如何檢索數據的呢?我們圍繞這些問題來探討一下。
你認為應該如何查詢數據
上一節談到 InnoDB 引擎的時候聊過在 InnoDB 引擎是面向行存儲的,數據都是存儲在磁盤的數據頁中,數據頁里面按照固定的行格式存儲着每一行數據。
InnoDB存儲引擎是 B+ 樹索引組織的,所以數據即索引,索引即數據。B+ 樹的葉子節點存儲的都是數據段的數據。InnoDB 引擎對數據的存儲必須依賴於主鍵,主鍵對應的索引叫做聚集索引。如果不幸的是你建表沒有建主鍵,InnoDB 會從表字段中尋找第一個非空的唯一索引作為聚集索引,如果還是不幸找不到,InnoDB 會生成一個不可見的名為 ROW_ID 的列,該列是一個 6 字節的自增數字,用來創建聚集索引。
小Tips:
對於 ROW_ID 列的自增實現其實是來自於一個全局自增序列,這意味着所有使用到 ROW_ID 作為聚集索引的表都共享該序列,如果在高並發的情況就有保證不了唯一性的可能。
大家都知道 MYSQL 中索引是使用 B+ 樹的數據結構,在此也就不故弄玄虛。但是大家有沒有想過除了 B+ 樹還有什么數據結構也可以用於索引檢索呢?我們不妨來看看。
二叉樹
對於二叉樹而言,每個節點只能有兩個子節點,如果是一顆單邊二叉樹,查詢某個節點的次數與節點所處的高度相同,時間復雜度為O(n);如果是一顆平衡二叉樹,查找效率高出一半,時間復雜度為O(Log2n)。
並且二叉樹還有另一個壞處,二叉樹上的每一個節點都是數據節點,那么對於一個比較高的數如果要獲取最下面的數據遍歷的節點數將會很消耗性能。
Hash 表
散列表的好處是散列查詢單條數據比較快,但是壞處也比較多,比如 Hash 碰撞的解決,范圍查找等等。
B 樹
B樹是二叉樹的升級版,又叫平衡多路查找樹。它和平衡二叉樹的區別在於:
- 平衡二叉樹最多兩個子樹,而 B 樹每個節點都可以有多個子樹,M 階 B 樹表示每個節點最多有M個子樹。
- 平衡二叉樹每個節點只有一個數據和兩個指向孩子的指針,而 B 樹每個中間節點有 k-1 個關鍵字(可以理解為數據)和 k 個子樹( k 介於階數 M 和 M/2 之間,M/2 向上取整)。
- 所有葉子節點均在同一層、葉子節點除了包含關鍵字和關鍵字記錄的指針外也有指向其子節點的指針,只不過其指針地址都為 null 。
另外,它們相同的點是節點數據也是按照左小右大的順序排列。我們用一張圖來對比它們的區別:

B+ 樹
說到 B 樹就連着B+樹一起說了。B+ 樹是應文件系統所需而產生的一種 B 樹的變形樹(文件的目錄一級一級索引,只有最底層的葉子節點(文件)保存數據)非葉子節點只保存索引,不保存實際的數據)。

這里有一個很重要的一點就是 B+ 樹的非葉子節點不保存數據,只有索引。一棵 m 階的 B+ 樹和 m 階的 B 樹的異同點在於:
- 節點的子樹數和關鍵字數相同(B 樹是關鍵字數比子樹數少一);
- 所有的葉子結點中包含了全部關鍵字的信息,及指向含有這些關鍵字記錄的指針(節點的關鍵字表示的是子樹中的最大數,在子樹中同樣含有這個數據),且葉子結點本身依關鍵字的大小自小而大的順序鏈接。 (而 B 樹的葉子節點並沒有包括全部需要查找的信息);
- 所有的非終端結點可以看成是索引部分,結點中僅含有其子樹根結點中最大(或最小)關鍵字。 (而 B 樹的非終節點也包含需要查找的有效信息)。
- 葉子節點之間通過指針連接。
對於 B 樹和 B+ 樹來說,兩種數據結構都是為了減少磁盤 I/O 讀寫過於頻繁而生,本身節點的個數是有限的,采用多叉結構就是為了讓每一層放盡可能多的節點以此來降低整棵樹的高度。但是為什么 InnoDB 索引結構最終選擇了 B+ 樹而不是B 樹呢?
B+ 樹的磁盤讀寫代價更低
B+ 樹內部非葉子節點本身並不存儲數據,所以非葉子節點的存儲代價相比 B 樹就小的多。存儲容量減少同時也縮小了占用盤塊的數量,那么數據的聚集程度直接也影響了查詢磁盤的次數。
B+ 樹查詢效率更加穩定
樹高確定的前提下所有的數據都在葉子節點,那么無論怎么查詢所有關鍵字查詢的路徑長度是固定的。
B+ 樹對范圍查詢的支持更好
B+ 樹所有數據都在葉子節點,非葉子節點都是索引,那么做范圍查詢的時候只需要掃描一遍葉子節點即可;而 B 樹因為非葉子節點也保存數據,范圍查詢的時候要找到具體數據還需要進行一次中序遍歷。
MyISAM 和 InnoDB 索引組織的區別
在 MYSQL 中索引屬於存儲引級別的概念,存儲引擎不同,索引的實現方式也不一樣。我們分別看看看 MyISAM 和 InnoDB 中都是如何實現索引功能。
MyISAM 實現
MyISAM 也是使用 B+ 樹作為索引存儲結構,他的葉子節點 data 域存放的是數據的物理地址,即索引結構和真正的數據結構其實是分開存儲的。

InnoDB 索引實現
MyISAM 索引和數據是分離的,但是在 InnoDB 中卻大不相同,InnoDB 中采用主鍵索引的方式,所有的數據都保存在主鍵索索引中。

所以這也是為什么 InnoDB 要求每個表都必須要有主鍵的原因。本身就是基於主鍵來組織的數據存儲。
索引類型
以下所有索引類型都是基於 InnoDB 引擎。
主鍵索引
主鍵索引也就是我們說的聚集索引。上面說過主鍵索引是基於主鍵來創建的 B+ 樹索引結構,如果沒有指定主鍵,也找不到任何一列不重復的列可以作為主鍵的情況下,InnoDB 會新增一個隱藏列 RowId 作為主鍵繼而創建聚集索引。
二級索引(非主鍵索引)
二級索引就是指除了主鍵索引外的索引。主鍵索引和所有的二級索引都是各自維護各自的 B+ 樹結構,但是有個不同的地方在於,二級索引的葉子節點存儲的不是數據,而是主鍵索引對應的主鍵值。
即二級索引不再保存一份 data 數據,而是去主鍵索引中查數據。那么對於二級索引查找一條數據索要做的操作就是:
- 首先在二級索引中找到葉子節點對應的數據主鍵值;
- 根據這個主鍵值去聚集索引中找到真正對應的數據行。
所以這里需要兩次 B+ Tree 查找。
覆蓋索引
覆蓋索引簡單來說就是只查詢索引就能獲取到數據不必再回表查詢,換句話說要查詢的列已經被索引列覆蓋。
使用覆蓋索引有如下優點:
- 索引項通常比記錄要小,所以 MySQL 訪問更少的數據;
- 索引都按值的大小順序存儲,相對於隨機訪問記錄,需要更少的 I/O;
- 大多數據引擎能更好的緩存索引。比如 MyISAM 只緩存索引;
- 覆蓋索引對於 InnoDB 表尤其有用,因為 InnoDB 使用聚集索引組織數據,如果二級索引中包含查詢所需的數據,就不再需要在聚集索引中查找了。
- 覆蓋索引不能是任何索引,只有 B Tree 索引存儲相應的值。而且不同的存儲引擎實現覆蓋索引的方式都不同,並不是所有存儲引擎都支持覆蓋索引( Memory 和 Falcon 就不支持)。
聯合索引
有的時候我們會對多個列建立一個索引,這種索引被稱為聯合索引。而關於聯合索引的建立和使用,從工作開始你的各位 “師長” 都在教導你要遵循 “左前匹配原則”,那到底是為什么呢?什么是左前匹配原則呢?
比如我們有這樣一張表:
CREATE TABLE `test_tb` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`a` varchar(10) NOT NULL,
`b` varchar(10) NOT NULL,
`c` varchar(10) NOT NULL,
`d` int(10) NOT NULL,
`e` int(10) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_a_b_c` (`a`,`b`,`c`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
上表建立了一個聯合索引:idx_a_b_c。下面給出一個 SQL, 大家看它會不會走索引查詢:
select * from test_tb where b = '10';
很顯然根據 “左前匹配原則” 肯定不會走索引查詢,最終還是全表掃描。
原因就在於聯合索引的結構上。上面對 a,b,c 三個字段建立索引,那么對應的 B+ Tree 索引結構每個節點其實是按照三個字段的前后順序排列的,即 a 字段檢索在最前面,然后是b,然后是c。如果你的查詢不是按照這個順序來檢索,是不會被這個索引識別的。
左前匹配原則
上面說到聯合索引會遵循左前匹配原則,那么什么是左前匹配呢?
其實就是字面意義上的從建立索引的第一個字段開始先匹配查詢條件,如果當前查詢條件不是第一個字段那么就不會走該索引。
另外對於聯合索引的使用也有一些限制,比如說:
遇到范圍查詢 ( > ,<, between, like) 就會停止匹配
比如哦我們看這個 SQL:
select * from test_tb where a = '1' and b = '3' and d < 20 and c = '5';
大家覺得這個 SQL 會如何使用索引呢?
其實這 SQL 在前面a,b的查詢中是會走聯合索引的,但是在經歷了d的查詢之后,到了c就不會使用索引了,因為d的查詢已經將索引的順序打亂了,從 d 條件過后就沒有辦法直接使用聯合索引。
在索引列上做操作(函數,自定義計算)
同樣對索引列做計算也是無法直接應用索引,不言自明,索引是對已有的數據進行歸納排序,你計算之后的數據是新的內容,索引並沒有包含這些數據,無從查起。
查詢條件包含 or,可能導致索引失效
比如有一張 user 表:
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`userId` int(11) NOT NULL,
`age` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_userId` (`userId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
如下查詢語句:
select * from user where userId=3 or age > 20;
大家覺得這句會走索引嗎?
我們可以分析一下,如果 userId 查詢走了索引這沒問題,但是遇到 age > 20 這肯定沒法走索引,那么前面 userId 走索引做一次 索引掃描就沒有意義,所以優化器認為這種情況下不如一開始就走全表掃描更省事。
但是如果 or 前后的兩個字段都加了索引那么可能會走,這種要依情況而定。
字符串類型的索引字段作為查詢條件時一定要用引號括起來,否則索引不生效
上面我們沒有說的一點是,B+Tree 索引結構的 key 都是用數字表示的,因為數字比較省空間,就算是字符串格式的字段,最終也會被轉為二進制表示。但是對於不加引號的字符串,MSYQL 會自動做一次隱式轉換將字符串轉為浮點類型,這就導致不匹配。
like 通配符可能導致索引失效
使用 like 模糊查詢並不是所有的都會失效,只有以 “%” 開頭的 like 查詢才會失效。
左連接查詢或者右連接查詢查詢關聯的字段編碼格式不一樣,可能導致索引失效。
這個比較不常見,一般來說同一個庫中的表使用的編碼格式應該是一樣的,但是不排除老項目新老表有區別。
索引的缺點
上面一直在談論索引的優點,凡事有利就有弊,它也不是沒有缺點的:
-
磁盤空間占用。這個對於當前磁盤比買菜還便宜的硬件大通貨時代其實算不上問題,但是要注意的是如果當前 MySQL 服務所在的機器有很多的大表,並且還創建了每一種可能的組合的索引,那么索引文件提及的增長可能超乎你的想象。
-
維護索引對更新類操作所帶來的耗時。當對索引涉及到的列做更新或者新增操作時都會去維護相關的索引,這里也是一個耗時的點,所以索引不在多,而在精。
檢查一條 SQL 是否是 bad SQL - 執行計划
在 MySQL 中如何知道一條 sql 到底有沒有用到索引呢?MySQL 提供了 explain 關鍵字來查詢一條 sql 的執行效率。
比如我們有一張 user 表:
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`userId` int(11) NOT NULL,
`age` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_userId` (`userId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
查詢下面 sql 的查詢效率:
mysql> explain select * from user where id = 3;
+----+-------------+-------+------+---------------+------+---------+------+--------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------+
| 1 | SIMPLE | user | const | PRIMARY | PRIMARY | 4 | const | 1 | NULL |
+----+-------------+-------+------+---------------+------+---------+------+--------+-------+
1 row in set (0.04 sec)
執行計划各個字段的含義如下:
| 列名 | 含義 |
|---|---|
| id | 執行序號,MSYQL 會按照從大到小的順序執行 |
| select_type | 查詢類型:SIMPLE: 簡單查詢 PRIMARY: 外層查詢 SUBQUERY: 子查詢 DERIVED: 派生查詢(FROM 中包含的子查詢) UNION: UNION 中第二個或后面的那個查詢 UNION RESULT: UNION 的結果 |
| table | 引用的表 |
| partitions | 所屬分區 |
| type | 訪問類型官方文檔,常見訪問類型: system : 只有一條記錄的表(=系統表) const : 通過索引一次就查詢到 eq_ref : 唯一索引等值掃描 ref : 非唯一索引等值掃描 range : 范圍索引掃描 index : 索引掃描 all : 全表掃描 |
| possible_keys | 可能使用的索引(優化前) |
| key | 實際使用的索引(優化后) |
| key_len | 使用索引的長度 |
| ref | 上述表的連接匹配條件(哪些列或常量被用於查找索引列上的值) |
| rows | 必須掃描的行數 |
| Extra | 附加信息官方文檔,常見附加信息: Using filesort : mysql 無法利用索引完成排序操作 Using temporary : 使用了臨時表保存中間結果 Using index : select 操作使用了覆蓋索引 Using where : 使用 where 過濾 using join buffer : 使用了連接緩存 impossible where : where 子句的值總是 false,不能用來獲取任何記錄 distinct : 優化 distinct,在找到第一個匹配的記錄后停止掃描同樣值的動作 |
這么多字段我們挑幾個重點來解釋一下:
id
執行序號,id 列的編號是 select 的序列號,有幾個 select 就有幾個 id,並且 id 的順序是按select 出現的順序增長的。id 越大執行優先級越高,id 相同則從上往下執行,id 為 NULL 最后執行。
key_len
key_len 長度表示在索引里使用的字節數,通過這個值可以估算出具體使用了索引中的哪些列。
ken_len 計算規則如下:
- 字符串 :char(n):n 字節長度; varchar(n):n 字節存儲字符串長度,如果是 utf-8, 則長度是 3n+2,這里的長度與字符集有直接關系;
- 數值類型:tinyint:1 字節;smallint:2 字節 ;int:4 字節; bigint:8字節;
- 時間類型 :date:3字節;timestamp:4字節;datetime:8字節。
如果字段允許為 NULL,需要 1 字節記錄是否為 NULL; 索引最大長度是 768 字節,當字符串過長時,MySQL 會做一個類似做前綴索引的處理,將前半部分的字符串提取出來做索引。
type
type 顯示的是訪問類型,結果值從好到壞依次是:
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
一般來說,得保證查詢至少達到 range 級別,最好能達到 ref。
下面對這幾個類型簡要說明:
-
system
該表只有一行(如:系統表)。這是 const 連接類型的特例。 -
const
該表最多只有一個匹配行,在整個查詢過程中這個表最多只會有一條匹配的行,用到了 primary key 或者unique 索引。比如主鍵查詢肯定只有一條記錄被匹配到。
-
eq_ref
對於前面表格中的每個行組合,從該表中讀取一行。除了 system 和 const 類型之外,這是最好的連接類型。當連接使用索引的所有部分且索引是 索引PRIMARY KEY或UNIQUE NOT NULL索引時使用它。SELECT * FROM ref_table,other_table WHERE ref_table.key_column=other_table.column; -
ref
表示上述表的連接匹配條件,即哪些列或常量被用於查找索引列上的值。 -
fulltext
使用 FULLTEXT 索引執行連接。 -
ref_or_null
該連接類型如同 ref,但是添加了 MySQL 可以專門搜索包含 NULL 值的行。
SELECT * FROM ref_table WHERE key_column IS NULL; -
index_merge
該連接類型表示使用了索引合並優化方法。SELECT * FROM tbl_name WHERE key1 = 10 OR key2 = 20; SELECT * FROM tbl_name WHERE (key1 = 10 OR key2 = 20) AND non_key = 30; -
unique_subquery
此類型替換 以下形式的 eq_ref 某些 IN子查詢:value IN (SELECT primary_key FROM single_table WHERE some_expr) -
index_subquery
此連接類型類似於 unique_subquery。它替換 IN 子查詢,但它適用於以下形式的子查詢中的非唯一索引:value IN (SELECT key_column FROM single_table WHERE some_expr) -
range
給定范圍內的檢索,使用一個索引來檢查行。通常發生在在索引列上使用范圍查詢,如 >,<,in 等時,非索引列是 ALL。 -
index
按索引次序掃描,先讀索引,再讀實際的行,結果也是全表掃描,主要優點是避免了排序。(索引是排好序的,並且 all 是從硬盤中讀的,index 可能不在硬盤上。s -
ALL
對前面表格中的每個行組合進行全表掃描。如果表是第一個未標記的表 const,通常不好,並且在所有其他情況下通常 非常糟糕。通常,您可以ALL通過添加基於常量值或早期表中的列值從表中啟用行檢索的索引來避免
row
這一列是 MYSQL 估計要讀取並檢測的行數,注意這個不是結果集的行數。
Extra
Extra 是 EXPLAIN 輸出中另外一個很重要的列,該列顯示 MySQL 在查詢過程中的一些詳細信息。
- Distinct:MySQL 發現第 1 個匹配行后,停止為當前的行組合搜索更多的行。
- Not exists:MySQL 能夠對查詢進行 LEFT JOIN 優化,發現1個匹配 LEFT JOIN 標准的行后,不再為前面的的行組合在該表內檢查更多的行。
- range checked for each record:MySQL 沒有發現好的可以使用的索引,但發現如果來自前面的表的列值已知。可能部分索引可以使用。
- Using filesort:看到這個的時候,查詢就需要優化了。MySQL 需要進行額外的步驟來發現如何對返回的行排序。它根據連接類型以及存儲排序鍵值和匹配條件的全部行的行指針來排序全部行。
- Using index:從只使用索引樹中的信息而不需要進一步搜索讀取實際的行來檢索表中的列信息。
- Using temporary:為了解決查詢,MySQL 需要創建一個臨時表來容納結果。看到這個就需要進行優化了,這通常發生在對不同的列集進行 order by 上,而不是 group by 上。
- Using where:WHERE 子句用於限制哪一個行匹配下一個表或發送到客戶。
- Using sort_union| Using union|Using intersect:這些函數說明如何為 index_merge 聯接類型合並索引掃描。
- Using index for group-by:類似於訪問表的 Using index 方式,Using index for group-by 表示 MySQL 發現了一個索引,可以用來查詢 GROUP BY 或 DISTINCT 查詢的所有列,而不要額外搜索硬盤訪問實際的表。
了解了執行計划,當你不確定一條 sql 查詢效率的時候 就可以使用 Explain 來查看。
