介紹#
建立在多列上的索引成為聯合索引。
比如,將商品表中的 product_no 和 name 欄位組合成聯合索引 (product_no, name)
CREATE INDEX index_product_no_name ON product(product_no, name);
下圖為聯合索引(product_no, name)的 B+Tree 示意圖(葉子節點畫的是單向鏈表,實際為雙向,圖片有誤):
聯合索引的非葉子節點用兩個欄位的值作為 B+Tree 的 key 值。
聯合索引查詢資料時,先按 product_no 比較,在 product_no 相同的情況下再按 name 欄位比較。也就是說,聯合索引查詢的 B+Tree 是先按 product_no 進行排序,然後在 product_no 相同的情況下再按 name 欄位排序。
因此,使用聯合索引時,存在最左匹配原則:按照最左邊欄位優先的方式進行索引的匹配,如果不遵守,聯合索引就失效。
比如,創建了一個 (a,b,c) 的聯合索引,查詢條件是以下幾種,就可以匹配上聯合索引:
- where a=1;
- where a=1 and b=2 and c=3;
- where b=2 and a=1;
需要注意的是,因為有查詢優化器,所以 a 欄位在 where 子句的順序並不重要
但是,如果是以下幾種,因為不符合 [最左匹配原則],所以無法匹配上聯合索引:
- where b=2;
- where c=3;
- where b=2 and c=3;
上面這些**查詢條件之所以會失效,是因為(a, b, c) 聯合索引,是先按 a 排序,在 a 相同的情況再按 b 排序,在 b 相同的情況再按 c 排序。**
所以,b 和 c 是全局無序,局部相對有序的,這樣在沒有遵循最左匹配原則的情況下,是無法利用到索引的。
這裡舉例聯合索引(a, b)的 B+ Tree 如下(葉子節點畫的是單向鏈表,實際為雙向,圖片有誤):
可以看到,a 是全局有序的(1, 2, 2, 3, 4, 5, 6, 7 ,8),而 b 是全局是無序的(12,7,8,2,3,8,10,5,2)。因此,直接執行 where b = 2
這種查詢條件沒有辦法利用聯合索引的,利用索引的前提是索引裡的 key 是有序的。
只有在 a 相同的情況下,b 才是有序的,比如 a 等於 2 的時候,b 的值為(7,8),這時就是有序的,這個有序狀態是局部的,因此,執行 where a = 2 and b = 7
時 a 和 b 欄位能用到聯合索引,也就是聯合索引生效了。
聯合索引範圍查詢#
聯合索引有一些特殊情況,並不是查詢過程使用了聯合索引查詢,就代表聯合索引中的所有欄位都用到了聯合索引進行索引查詢,可能存在部分欄位用到聯合索引的 B+Tree,部分欄位沒有用到的情況,這種特殊情況就發生在範圍查詢。
聯合索引的最左匹配原則會一直向右匹配直到遇到「範圍查詢」就會停止匹配。也就是範圍查詢的欄位可以用到聯合索引,但是在範圍查詢欄位的後面的欄位無法用到聯合索引。
導致聯合索引的最左匹配原則停止匹配的範圍查詢#
Q1: select * from t_table where a > 1 and b = 2
,聯合索引 (a, b) 哪個欄位用到了聯合索引的 B+Tree?#
只有 a 欄位用到了聯合索引進行索引查詢,而 b 欄位並沒有使用到。
在符合 a > 1 條件的二級索引級聯的範圍裡,b 欄位的值是無序的。因此,無法根據 b = 2 來進一步減少需要掃描的級聯數量。
透過執行計劃中的 key(使用到的索引) 和 key_len(使用了索引中幾個欄位) 得知優化器具體使用了多少個欄位的搜索條件來形成掃描區間的邊界條件。
EXPLAIN select * from t_table where a > 1 and b = 2
a 和 b 都是 int 且不為 NULL,int 占用 4 字節(如果欄位允許為 NULL,就在欄位類型占用的字節數上加 1,也就是 5)下圖中 key_len 為 4,代表只有 a 欄位用到了聯合索引:
Q2:select * from t_table where a ≥ 1 and b = 2
,聯合索引 (a, b) 哪個欄位用到了聯合索引的 B+Tree?#
雖然在符合 a>= 1 條件的二級索引記錄的範圍裡,b 欄位的值是「無序」的,但是對於符合 a = 1 的二級索引記錄的範圍裡,b 欄位的值是「有序」的(因為對於聯合索引,是先按照 a 欄位的值排序,然後在 a 欄位的值相同的情況下,再按照 b 欄位的值進行排序)。也就是說,從符合 a = 1 and b = 2 條件的第一條記錄開始掃描,而不需要從第一個 a 欄位值為 1 的記錄開始掃描。
所以,Q2 這條查詢語句 a 和 b 欄位都用到了聯合索引進行索引查詢。
透過執行計劃中的 key_len 可以得知。
Q3:select * from t_table where a between 2 and 8 and b =2
,聯合索引 (a, b) 哪個欄位用到了聯合索引的 B+Tree?#
在 MySQL 中,BETWEEN 包含了 value1 和 value2 邊界值,類似於 >= and =<。而有的資料庫則不包含 value1 和 value2 邊界值(類似於> and <)。
由於 MySQL 的 BETWEEN 包含 value1 和 value2 邊界值,所以類似於 Q2 查詢語句,因此 Q3 這條查詢語句 a 和 b 欄位都用到了聯合索引進行索引查詢。
透過執行計劃中的 key_len 可以得知。
Q4:select * from t_user where name like ‘j%’ and age = 22
,聯合索引 (name, age) 哪個欄位用到了聯合索引的 B+Tree?#
a 欄位可以在聯合索引的 B+Tree 中進行索引查詢,形成的掃描區間是 ['j','k')。注意, j 是閉區間(即≥=j and <k 的區間)。如下圖:
雖然在符合前綴為 ‘j’ 的 name 欄位的二級索引記錄的範圍裡,age 欄位的值是「無序」的,但是對於符合 name = j 的二級索引記錄的範圍裡,age 欄位的值是「有序」的(因為對於聯合索引,是先按照 name 欄位的值排序,然後在 name 欄位的值相同的情況下,再按照 age 欄位的值進行排序)。
也就是說,從符合 name = 'j' and age = 22
條件的第一條記錄時開始掃描,而不需要從第一個 name = 'j'
的記錄開始掃描 。如下圖的右邊:
所以,Q4 這條查詢語句 a 和 b 欄位都用到了聯合索引進行索引查詢。
透過執行計劃中的 key_len 可以知道:
-
name 欄位類型是 varchar (30) 且不為 NULL,資料庫表使用了 utf8mb4 字符集,一個字符是 4 個字節,因此 name 最多占用 120 字節(30*4),又因為 name 是變長類型的欄位,需要再加 2 字節(用於存儲該欄位實際資料的長度值),最終得知 name 的 ken_len 為 122
-
age 欄位類型是 int 且不為 NULL,ken_len 為 4
TIP
對於「因為 name 是變長類型的欄位,需要再加 2 字節」。之前這篇 **文章 (opens new window)** 說「如果變長欄位允許存儲的最大字節數小於等於 255 字節,就會用 1 字節表示變長欄位的長度」,而這裡為什麼是 2 字節?
key_len 的顯示比較特殊,行格式是由 innodb 存 储引擎實現的,而執行計劃是在 server 層生成的,所以它不會去問 innodb 存儲引擎可變欄位的長度占用多少字節,而是不管三七二十一都使用 2 字節表示可變欄位的長度。
畢竟 key_len 的目的只是為了告訴你索引查詢中用了哪些索引欄位,而不是為了準確告訴這個欄位占用多少字節空間。
Q4 查詢語句的執行計劃如下,可以看到 key_len 為 126 字節,name 的 key_len 為 122,age 的 key_len 為 4,說明優化器使用了 2 個欄位的查詢條件來形成掃描區間的邊界條件,也就是 name 和 age 欄位都用到了聯合索引進行索引查詢。
綜上所述,聯合索引的最左匹配原則,在遇到範圍查詢(如 >、<)時停止匹配,即範圍查詢的欄位可以用到聯合索引,但其後的欄位無法用到。注意,對於 ≥、≤、between、”like 前綴匹配” 的範圍查詢並不會停止匹配。
索引下推#
對於聯合索引 (a, b),在執行 select * from table where a > 1 and b = 2;
語句時,只有 a 欄位能用到索引,在聯合索引的 B+Tree 找到第一個滿足條件的主鍵值(ID 為 2)後,還需要判斷其他條件是否滿足(即 b = 2),這時是在聯合索引裡判斷?還是回主鍵索引去判斷?
- MySQL 5.6 之前,只能從 ID2(主鍵值)開始一個個回表,到 [主鍵索引] 上找出資料行,再對比 b 欄位值
- MySQL 5.6 引入的索引下推優化 (index condition pushdown),可以在聯合索引遍歷過程中,在聯合索引中對包含的欄位(該欄位在聯合索引的列中)先做判斷(b=2),直接過濾掉不滿足條件的記錄,按照過濾後的資料再一一進行回表掃描,以此減少回表次數
當查詢語句的執行計劃裡,出現了 Extra 為 Using index condition
,就說明使用了索引下推的優化。
索引區分度#
建立聯合索引時的欄位順序,對索引效率也有很大影響。越靠前的欄位被用於索引過濾的概率越高,實際開發工作中建立聯合索引時,要把區分度大的欄位排在前面,這樣區分度大的欄位越有可能被更多的 SQL 使用到。
區分度就是某個欄位 column 不同值的個數「除以」表的總行數,計算公式如下:
比如,性別的區分度就很小,不適合建立索引或不適合排在聯合索引列的靠前的位置,而 UUID 這類欄位就比較適合做索引或排在聯合索引列的靠前的位置。
因為如果索引的區分度很小,假設欄位的值分佈均勻,那麼無論搜索哪個值都可能得到一半的資料。在這些情況下,還不如不要索引,因為 MySQL 還有一個查詢優化器,查詢優化器發現某個值出現在表的資料行中的百分比(慣用的百分比界線是 "30%")很高的時候,它一般會忽略索引,進行全表掃描。
聯合索引進行排序#
select * from order where status = 1 order by create_time asc
針對以上 SQL,建立 (status, create_time) 聯合索引,可避免 MySQL 資料庫發生文件排序(filesort)。
在查詢時,如果只用到 status 的索引,但是這條語句還要對 create_time 排序,這時就要用文件排序 filesort,也就是在 SQL 執行計劃中,Extra 列會出現 Using filesort。
所以,要利用索引的有序性,在 status 和 create_time 欄位建立聯合索引,這樣根據 status 篩選後的資料就是按照 create_time 排好序的,避免再文件排序,提高了查詢效率。
總結一句話:避免 using filesort 就使用覆蓋索引。