標籤: 公衆號文章mysql
對於開發小夥伴來講,對MySQL
中的包含IN子句的語句確定熟悉的不能再熟悉了,幾乎每天用,時時用。但是不少小夥伴並不知道包含IN子句的語句是怎樣執行的,在一些查詢優化的場景中就開始找不着北了,本篇文章就來嘮叨一下MySQL中包含IN子句的語句是怎樣執行的(以MySQL 5.7的InnoDB存儲引擎爲例)。算法
爲了故事的順利發展,咱們先建立一個表:sql
CREATE TABLE t (
id INT NOT NULL AUTO_INCREMENT,
key1 VARCHAR(100),
common_field VARCHAR(100),
PRIMARY KEY (id),
KEY idx_key1 (key1)
) Engine=InnoDB CHARSET=utf8;
複製代碼
能夠看到表t
中包含兩個索引:bash
id
列爲主鍵的聚簇索引key1
列創建的二級索引這個表裏邊如今有10000條數據:優化
mysql> SELECT COUNT(*) FROM t;
+----------+
| COUNT(*) |
+----------+
| 10000 |
+----------+
1 row in set (0.00 sec)
複製代碼
咱們如今想執行下邊這個語句:ui
SELECT * FROM t WHERE
key1 >= 'b' AND key1 <= 'c';
複製代碼
假設優化器選擇使用二級索引來執行查詢,那麼查詢語句的執行示意圖就以下圖所示:spa
小貼士:原諒我把索引對應的複雜的B+樹結構搞了一個極度精簡版,爲了突出重點,咱們忽略掉了頁的結構,直接把全部的葉子節點的記錄都放在一塊兒展現。咱們想突出的重點就是:B+樹葉子節點中的記錄是按照索引列值大小排序的,對於的聚簇索引來講,它對應的B+樹葉子節點中的記錄就是按照id列排序的,對於idx_key1二級索引來講,它對應的B+樹葉子節點中的記錄就是按照key1列排序的。 3d
咱們想查詢key1
列的值在['b', 'c']
這個區間中的記錄,那麼就須要:code
先經過idx_key1
索引對應的B+
樹快速定位到key1
列值爲'b'
、而且最靠左的那條二級索引記錄,該二級索引記錄中包含着對應的主鍵值,根據這個主鍵值再到聚簇索引中定位到完整的記錄(這個過程稱之爲回表),將其返回給server層,server層再發送給客戶端。cdn
記錄按照鍵值由小到大的順序排列成一個單鏈表的形式,因此咱們能夠沿着這個單鏈表接着定位到下一條二級索引記錄,而且執行回表操做,將完整的記錄交給server層以後發送給客戶端。
繼續沿着記錄的單向鏈表查找,重複上述過程,直到找到的二級索引記錄的key1列的值不知足key1 <= 'c'
的這個條件,如圖所示,也就是當咱們在idx_key1
二級索引中找到了key1='ca'
的那條記錄後,發現它不符合key1 <= 'c'
的條件,因此就中止查找。
上述過程就是經過B+
樹查找一個鍵值在某一個範圍區間的記錄的過程。
若是咱們想執行下邊這個語句:
SELECT * FROM t WHERE
key1 IN ('b', 'c');
複製代碼
若是優化器選擇使用二級索引執行上述語句,那它是如何執行的呢?
優化器會將IN子句中的條件當作是2個範圍區間(雖然這兩個區間中都僅僅包含一個值):
['b', 'b']
['c', 'c']
那麼在語句執行過程當中就須要經過B+
樹去定位兩次記錄所在的位置:
先定位鍵值在範圍區間['b', 'b']
的記錄:
先經過idx_key1
索引對應的B+
樹快速定位到key1
列值爲'b'
、而且最靠左的那條二級索引記錄,以後回表將其發送給server 層後再發送給客戶端。
再沿着記錄組成的單鏈表把符合key1=b
的二級索引記錄找到,而且回表後發送給server層,以後再發送給客戶端。
重複上述過程,直到找到的二級索引記錄的key1列的值不知足key1 = 'b'
的這個條件爲止。
再定位鍵值在範圍區間['c', 'c']
的記錄:
查找過程相似,就很少贅述了。
因此若是你寫的IN語句中的參數越多,意味着須要經過B+
樹定位記錄的次數就越多。
比方說下邊這條語句:
SELECT * FROM t WHERE
key1 IN ('b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b');
複製代碼
雖然IN子句中包含好多個參數,但MySQL在語法解析的時候只會爲其生成一個範圍區間,那就是:['b', 'b']
。
比方說下邊這條語句:
SELECT * FROM t WHERE key1 IN ('c', 'b');
複製代碼
IN ('c', 'b')和IN ('b', 'c')有啥差異麼?也就是存儲引擎在對待IN ('c', 'b')子句時,會先去找key1 = 'c'
的記錄,再去找key1 = 'b'
的記錄麼?若是是這樣的話,下邊兩條語句豈不是可能發生死鎖:
事務T1中的語句一:
SELECT * FROM t WHERE
key1 IN ('b', 'c') FOR UPDATE;
事務T2中的語句二:
SELECT * FROM t WHERE
key1 IN ('c', 'b') FOR UPDATE;
複製代碼
放心,在生成範圍區間的時候,天然是將範圍區間排了序,也就是即便條件是IN ('c', 'b')
,那優化器也會先讓存儲引擎去找鍵值在['b', 'b']
這個範圍區間中的記錄,而後再去找鍵值在['c', 'c']
這個範圍區間中的記錄。
你們必定要記着:MySQL優化器決定使用某個索引執行查詢的僅僅是由於:使用該索引時的成本足夠低。也就是說即便咱們有下邊的語句:
SELECT * FROM t WHERE
key1 IN ('b', 'c');
複製代碼
MySQL優化器須要去分析一下若是使用二級索引idx_key1
執行查詢的話,鍵值在['b', 'b']
和['c', 'c']
這兩個範圍區間的記錄共有多少條,而後經過必定方式計算出成本,與全表掃描的成本相對比,選取成本更低的那種方式執行查詢。
在計算查詢成本的這一步驟中你們須要注意,對於包含IN子句條件的查詢來講,須要依次分析一下每個範圍區間中的記錄數量是多少。MySQL優化器針對IN子句對應的範圍區間的多少而指定了不一樣的策略:
若是IN子句對應的範圍區間比較少,那麼將率先去訪問一下存儲引擎,看一下每一個範圍區間中的記錄有多少條(若是範圍區間的記錄比較少,那麼統計結果就是精確的,反之會採用必定的手段計算一個模糊的值,固然算法也比較麻煩,咱們就不展開說了,小冊裏有說MySQL是怎樣運行的小冊連接),這種在查詢真正執行前優化器就率先訪問索引來計算須要掃描的索引記錄數量的方式稱之爲index dive。
若是IN子句對應的範圍區間比較多,這樣就不能採用index dive的方式去真正的訪問二級索引idx_key1(由於那將耗費大量的時間),而是須要採用以前在背地裏產生的一些統計數據去估算匹配的二級索引記錄有多少條(很顯然根據統計數據去估算記錄條數比index dive的方式精確性差了不少)。
那何時採用index dive的統計方式,何時採用index statistic的統計方式呢?這就取決於系統變量eq_range_index_dive_limit的值了,咱們看一下在個人機器上該系統變量的值:
mysql> SHOW VARIABLES LIKE 'eq_range_index_dive_limit';
+---------------------------+-------+
| Variable_name | Value |
+---------------------------+-------+
| eq_range_index_dive_limit | 200 |
+---------------------------+-------+
1 row in set (0.20 sec)
複製代碼
能夠看到它的默認值是200,這也就意味着當範圍區間個數小於200時,將採用index dive的統計方式,不然將採用index statistic的統計方式。
不過這裏須要你們特別注意,在MySQL 5.7.3以及以前的版本中,eq_range_index_dive_limit的默認值爲10。因此若是你們採用的是5.7.3以及以前的版本的話,很容易採用索引統計數據而不是index dive的方式來計算查詢成本。當你的查詢中使用到了IN查詢,可是卻實際沒有用到索引,就應該考慮一下是否是因爲 eq_range_index_dive_limit 值過小致使的。
本文首發於公衆號「咱們都是小青蛙」。
寫文章挺累的,有時候你以爲閱讀挺流暢的,那實際上是背後無數次修改的結果。若是你以爲不錯請幫忙轉發一下,萬分感謝~ 這裏是個人公衆號「咱們都是小青蛙」,裏邊有更多技術乾貨,時不時扯一下犢子,歡迎關注: