對於聯合索引我們知道,在使用的時候有一個最左前綴的原則,除了這些呢,比如字段放置的位置,會不會對索引的效率產生影響呢?
最左匹配原則
聯合索引時會遵循最左前綴匹配的原則,即最左優先,在檢索數據時從聯合索引的最左邊開始匹配,示例:
create table test
(
id bigint auto_increment primary key,
column_1 bigint null,
column_2 bigint null,
column_3 bigint null
);
create index test_column_1_column_2_column_3_index
on test(column_1, column_2, column_3);
比如上面的test表,我們建立了聯合索引index test_column_1_column_2_column_3_index on test (column_1, column_2, column_3);當我們進行查詢的時候,按照最左前綴的原則,當查詢(column_1)、(column_1,column_2)(column_1,column_2,column_3)這三種組合是可以用到我們定義的聯合索引的。如果我們查詢(column_1,column_3)就只能用到column_1的索引了。我們不用太關心索引的先后順序,什么意思呢?比如使用(column_1,column_2)和(column_2,column_1)的效果是一樣的,數據庫的查詢優化器會自動幫助我們優化我們的sql,看哪個執行的效率最高,最后才生成最后執行的sql。
為什么會有最左前綴呢?
使用b+樹作為索引的存儲數據結構時,當我們創建聯合索引的時候,比如(column_1, column_2, column_3),b+樹建立索引是從左到右來建立搜索樹的,比如當我們來查詢的時候WHERE column_1 = 1 AND column_2 = 2 AND column_3 = 3。b+樹會先通過最左邊的(建立索引的字段的左邊的字段)字段,也就是column_1來確定下一步的查找對象,然后找到column_2,再通過column_2的索引找到column_3。所以(column_2,column_3)這樣的查詢命中不到索引了。因為最左前綴,一定是從最左邊的字段開始依次在b+樹的子節點查詢,然后確定下一個查找的子節點的數據。所以我們(column_1)、(column_1,column_2)、(column_1,column_2,column_3)這三種查詢條件是可以使用到索引的。
聯合索引的存儲結構
定義聯合索引(員工級別,員工姓名,員工出生年月),將聯合索引按照索引順序放入節點中,新插入節點時,先按照聯合索引中的員工級別比較,如果相同會按照員工姓名比較,如果員工級別和員工姓名都相同 最后是員工的出生年月比較。圖中從上到下,從左到右看,第一個B+樹的節點 是通過聯合索引的員工級別比較的,第二個節點是 員工級別相同,會按照員工姓名比較,第三個節點是 員工級別和員工姓名都相同,會按照員工出生年月比較。

聯合索引字段的先后順序
我們定義多個字段的聯合索引,會考慮到字段的先后順序。那么字段的先后順序真的會對查詢的效率產生影響嗎?比如上面的聯合索引index test_column_1_column_2_column_3_index on test (column_1, column_2, column_3);和index test_column_1_column_2_column_3_index on test (column_2, column_1, column_3);在查詢效率上有差別嗎?我們試驗下。
寫個函數批量插入下數據
CREATE PROCEDURE dowhile()
BEGIN
DECLARE v1 INT DEFAULT 20000000;
WHILE v1 > 0 DO
INSERT INTO test.test (column_1, column_2, column_3) VALUES (RAND() * 20000000, RAND() * 10000, RAND() * 20000000);
SET v1 = v1 - 1;
END WHILE;
END;
我們插入了20000000條數據,然后先設置索引(column_1, column_2, column_3)中column_1的數值范圍為0到20000000,column_2的范圍為0到10000。然后查詢,看看這個索引的效率。數據量太大,插入的時間可能要好久。為什么插入20000000條呢,因為b+樹可以高效存儲的數據條數就是21902400,具體見下文。
我們嘗試下查詢的效率:
SELECT * FROM test WHERE column_1=19999834 AND column_2=3601
> OK
> 時間: 0.001s
EXPLAIN SELECT * FROM test WHERE column_1=19999834 AND column_2=3601

我們看到索引的type為ref已經相當高效了。
type:這列最重要,顯示了連接使用了哪種類別,有無使用索引,是使用Explain命令分析性能瓶頸的關鍵項之一。
結果值從好到壞依次是:system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
一般來說,得保證查詢至少達到range級別,最好能達到ref,否則就可能會出現性能問題。
然后我們看下插入的效率:
INSERT INTO test.test (column_1, column_2, column_3) VALUES (RAND() * 20000000, RAND() * 10000, RAND() * 20000000)
> Affected rows: 1
> 時間: 0.002s
更改索引的順序:
drop index test_column_1_column_2_column_3_index on test;
create index test_column_2_column_1_column_3_index
on test (column_2, column_1, column_3);
我們把column_2和column_1的索引位置更換了一下,來比較聯合索引的先后順序對查詢效率的影響。
SELECT * FROM test WHERE column_2=3601 AND column_1=19999834
> OK
> 時間: 0.001s
EXPLAIN SELECT * FROM test WHERE column_2=3601 AND column_1=19999834

發現更換了之后查詢時間上沒有什么出入,還和上個查詢的時間一樣,分析查詢的效率一樣很高。
再來看插入的效率:
INSERT INTO test.test (column_1, column_2, column_3) VALUES (RAND() * 20000000, RAND() * 10000, RAND() * 20000000)
> Affected rows: 1
> 時間: 0.003s
依然高效。
所以我們可以總結出來,聯合索引中字段的先后順序,在sql層面的執行效率,差別不大,是可以忽略的。分析上面索引的數據結構也是可以推斷出來的,無非就是當建立聯合索引,更換索引字段的先后順序,匹配每個字段鎖定的數據條數不一樣,但是對最終的查詢效率沒有太大的影響。但是這個字段的順序真的就不用考慮嗎?不是的,我們知道有最左匹配原則,所以我們要考慮我們的業務,比如說我們的業務場景中有一個字段enterpriseId,這個字段在80%的查詢場景中都會遇到,那么我們肯定首選將這個字段放在聯合索引字段的第一個位置,這樣就能保證查詢的高效,能夠命中我們建立的索引。
b+樹可以存儲的數據條數
如圖,為B+樹組織數據的方式:

實際存儲時當然不會每個節點只存3條數據。
以InnoDB引擎為例,簡單計算一下一顆B+樹可以存放多少行數據。
B+樹特點:只有葉子節點存儲數據,而非葉子節點存放的是用來找到葉子節點數據的索引(如上圖:key和指針)
InnoDB存儲引擎的最小存儲單元為16k(就像操作系統的最小單元為4k 即1頁),在這即B+樹的一個節點的大小為16k
假設數據庫一條數據的大小為1k,則一個節點可以存儲16條數據
而非葉子節點,key一般為主鍵,假設8字節,指針在InnoDB中是6字節,一共為14字節,一個節點可以存儲 16384/14 = 1170個索引指針
可以算出一顆高度為2的樹(即根節點為存儲索引指針節點,還有1170個葉子節點存儲數據),每個節點可以存儲16條數據,一共1170*16條數據 = 18720條
高度為3的樹,可以存放 1170 * 1170 * 16 = 21902400條記錄
兩千多萬條數據,我們只需要B+樹為3層的數據結構就可以完成,通過主鍵查詢只需要3次IO操作就能查到對應記錄。
總結
對於聯合索引,我們不能忽略它的最左匹配原則,即在檢索數據時從聯合索引的最左邊開始匹配。對於創建聯合索引時,我們要根據我們的具體的查詢場景來定,聯合索引字段的先后順序,聯合索引字段的先后順序在sql層面上沒有太大差別,但是結合查詢的場景和最左匹配的原則,就能使一些查詢的場景不能很好的命中索引,這點使我們是不能忽略的。
| 參考: |
