本篇已收錄在 MySQL 是怎樣運行的 學習筆記系列
MySQL Server有一個稱為查詢優化器的模塊,一條查詢語句進行語法解析之后就會被交給查詢優化器來進行優化,優化的結果就是生成一個所謂的執行計划,這個執行計划表明了應該使用哪些索引進行查詢,表之間的連接順序是啥樣的,最后會按照執行計划中的步驟調用存儲引擎提供的方法來真正的執行查詢,並將查詢結果返回給用戶。但是在介紹查詢優化器之前, 我們需要先對單表查詢的優化進行一定程度上的掌握.
建表
CREATE TABLE single_table (
id INT NOT NULL AUTO_INCREMENT,
key1 VARCHAR(100),
key2 INT,
key3 VARCHAR(100),
key_part1 VARCHAR(100),
key_part2 VARCHAR(100),
key_part3 VARCHAR(100),
common_field VARCHAR(100),
PRIMARY KEY (id),
KEY idx_key1 (key1),
UNIQUE KEY idx_key2 (key2),
KEY idx_key3 (key3),
KEY idx_key_part(key_part1, key_part2, key_part3)
) Engine=InnoDB CHARSET=utf8;
訪問方法和訪問類型
MySQL查詢器在執行查詢語句的方式稱之為
訪問方法
或者訪問類型
訪問方法 const
通過主鍵列來定位一條記錄,比方說這個查詢:
SELECT * FROM single_table WHERE id = 1438;
根據唯一二級索引列來定位一條記錄的速度也是賊快的,比如下邊這個查詢:
SELECT * FROM single_table WHERE key2 = 3841;
第一步先從idx_key2對應的B+樹索引中根據key2列與常數的等值比較條件定位到一條二級索引記錄,然后再根據該記錄的id值到聚簇索引中獲取到完整的用戶記錄。
mysql 認為通過主鍵或者唯一二級索引列與常數的等值比較來定位一條記錄是像坐火箭一樣快的,所以他們把這種通過主鍵或者唯一二級索引列來定位一條記錄的訪問方法定義為:const,意思是常數級別的,代價是可以忽略不計的。
訪問方法 ref
對某個普通的二級索引列與常數進行等值比較,比如這樣:
SELECT * FROM single_table WHERE key1 = 'abc';
由於普通二級索引並不限制索引列值的唯一性,所以可能找到多條對應的記錄,也就是說使用二級索引來執行查詢的代價取決於等值匹配到的二級索引記錄條數。如果匹配的記錄較少,則回表的代價還是比較低的,所以MySQL可能選擇使用索引而不是全表掃描的方式來執行查詢。mysql 就把這種搜索條件為二級索引列與常數等值比較,采用二級索引來執行查詢的訪問方法稱為:ref.
二級索引列值為NULL的情況
不論是普通的二級索引,還是唯一二級索引,它們的索引列對包含NULL值的數量並不限制,所以我們采用key IS NULL這種形式的搜索條件最多只能使用ref的訪問方法,而不是const的訪問方法。
二級索引為聯合索引, 並且都是等值比較時的情況
對於某個包含多個索引列的二級索引來說,只要是最左邊的連續索引列是與常數的等值比較就可能采用ref的訪問方法,比方說下邊這幾個查詢:
SELECT * FROM single_table WHERE key_part1 = 'god like';
SELECT * FROM single_table WHERE key_part1 = 'god like' AND key_part2 = 'legendary';
SELECT * FROM single_table WHERE key_part1 = 'god like' AND key_part2 = 'legendary' AND key_part3 = 'penta kill';
但是如果最左邊的連續索引列並不全部是等值比較的話,它的訪問方法就不能稱為ref了,比方說這樣:
SELECT * FROM single_table WHERE key_part1 = 'god like' AND key_part2 > 'legendary';
訪問方法 ref_or_null
有時候我們不僅想找出某個二級索引列的值等於某個常數的記錄,還想把該列的值為NULL的記錄也找出來,就像下邊這個查詢:
SELECT * FROM single_table WHERE key1 = 'abc' OR key1 IS NULL;
當使用二級索引而不是全表掃描的方式執行該查詢時,這種類型的查詢使用的訪問方法就稱為ref_or_null,先分別從idx_key1索引對應的B+樹中找出key1 IS NULL和key1 = 'abc'的兩個連續的記錄范圍,然后根據這些二級索引記錄中的id值再回表查找完整的用戶記錄。
訪問方法 range
有時候我們面對的搜索條件更復雜,比如下邊這個查詢:
SELECT * FROM single_table WHERE key2 IN (1438, 6328) OR (key2 >= 38 AND key2 <= 79);
如果采用二級索引 + 回表的方式來執行的話,那么此時的搜索條件就不只是要求索引列與常數的等值匹配了,而是索引列需要匹配某個或某些范圍的值,在本查詢中key2列的值只要匹配下列3個范圍中的任何一個就算是匹配成功了:
key2的值是1438
key2的值是6328
key2的值在38和79之間
mysql 把這種利用索引進行范圍匹配的訪問方法稱之為:range.
訪問方法 index
看下邊這個查詢:
SELECT key_part1, key_part2, key_part3 FROM single_table WHERE key_part2 = 'abc';
由於key_part2並不是聯合索引idx_key_part最左索引列,所以我們無法使用ref或者range訪問方法來執行這個語句。但是這個查詢符合下邊這兩個條件:
它的查詢列表只有3個列:key_part1, key_part2, key_part3,而索引idx_key_part又包含這三個列。
搜索條件中只有key_part2列。這個列也包含在索引idx_key_part中。
也就是說我們可以直接通過遍歷idx_key_part索引的葉子節點的記錄來比較key_part2 = 'abc'這個條件是否成立,把匹配成功的二級索引記錄的key_part1, key_part2, key_part3列的值直接加到結果集中就行了。由於二級索引記錄比聚簇索記錄小的多(聚簇索引記錄要存儲所有用戶定義的列以及所謂的隱藏列,而二級索引記錄只需要存放索引列和主鍵),而且這個過程也不用進行回表操作,所以直接遍歷二級索引比直接遍歷聚簇索引的成本要小很多,MySQL 就把這種采用遍歷二級索引記錄的執行方式稱之為:index。
all
最直接的查詢執行方式就是我們已經提了無數遍的全表掃描,對於InnoDB表來說也就是直接掃描聚簇索引,MySQL把這種使用全表掃描執行查詢的方式稱之為:all。
注意事項
盡量少的回表
下邊的這個查詢:
SELECT * FROM single_table WHERE key1 = 'abc' AND key2 > 1000;
查詢優化器會識別到這個查詢中的兩個搜索條件:
key1 = 'abc'
key2 > 1000
優化器一般會根據single_table表的統計數據來判斷到底使用哪個條件到對應的二級索引中查詢掃描的行數會更少,選擇那個掃描行數較少的條件到對應的二級索引中查詢.
一個使用到索引的搜索條件和沒有使用該索引的搜索條件使用OR連接起來后是無法使用該索引的
SELECT * FROM single_table WHERE key2 > 100 OR common_field = 'abc';
我們把使用不到idx_key2索引的搜索條件替換為TRUE:
SELECT * FROM single_table WHERE key2 > 100 OR TRUE;
接着化簡:
SELECT * FROM single_table WHERE TRUE;
索引合並
Intersection合並
MySQL在某些特定的情況下才可能會使用到Intersection索引合並:
情況一:二級索引列是等值匹配的情況,對於聯合索引來說,在聯合索引中的每個列都必須等值匹配,不能出現只匹配部分列的情況。
情況二:主鍵列可以是范圍匹配
Union合並
Intersection是交集的意思,這適用於使用不同索引的搜索條件之間使用AND連接起來的情況;Union是並集的意思,適用於使用不同索引的搜索條件之間使用OR連接起來的情況。與Intersection索引合並類似,MySQL在某些特定的情況下才可能會使用到Union索引合並:
情況一:二級索引列是等值匹配的情況,對於聯合索引來說,在聯合索引中的每個列都必須等值匹配,不能出現只出現匹配部分列的情況。
情況二:主鍵列可以是范圍匹配
情況三:使用Intersection索引合並的搜索條件
查詢條件符合了這些情況也不一定就會采用Union索引合並,也得看優化器的心情。優化器只有在單獨根據搜索條件從某個二級索引中獲取的記錄數比較少,通過Union索引合並后進行訪問的代價比全表掃描更小時才會使用Union索引合並。
Sort-Union合並
Union索引合並的使用條件太苛刻,必須保證各個二級索引列在進行等值匹配的條件下才可能被用到,比方說下邊這個查詢就無法使用到Union索引合並:
SELECT * FROM single_table WHERE key1 < 'a' OR key3 > 'z'
這是因為根據key1 < 'a'從idx_key1索引中獲取的二級索引記錄的主鍵值不是排好序的,根據key3 > 'z'從idx_key3索引中獲取的二級索引記錄的主鍵值也不是排好序的,但是key1 < 'a'和key3 > 'z'這兩個條件又特別讓我們動心,所以我們可以這樣:
先根據key1 < 'a'條件從idx_key1二級索引中獲取記錄,並按照記錄的主鍵值進行排序
再根據key3 > 'z'條件從idx_key3二級索引中獲取記錄,並按照記錄的主鍵值進行排序
因為上述的兩個二級索引主鍵值都是排好序的,剩下的操作和Union索引合並方式就一樣了。
聯合索引替代Intersection索引合並
SELECT * FROM single_table WHERE key1 = 'a' AND key3 = 'b';
這個查詢之所以可能使用Intersection索引合並的方式執行,還不是因為idx_key1和idx_key3是兩個單獨的B+樹索引,你要是把這兩個列搞一個聯合索引,那直接使用這個聯合索引就把事情搞定了,何必用啥索引合並呢,就像這樣:
ALTER TABLE single_table drop index idx_key1, idx_key3, add index idx_key1_key3(key1, key3);
這樣我們把沒用的idx_key1、idx_key3都干掉,再添加一個聯合索引idx_key1_key3,使用這個聯合索引進行查詢簡直是又快又好,既不用多讀一棵B+樹,也不用合並結果. 不過小心有單獨對key3列進行查詢的業務場景,這樣子不得不再把key3列的單獨索引給加上。