1. 程式人生 > 其它 >MYSQL IN 是否走索引?

MYSQL IN 是否走索引?

準備工作

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中包含兩個索引:

  • id列為主鍵的聚簇索引
  • key1列建立的二級索引

這個表裡邊現在有10000條資料:

mysql> SELECT COUNT(*) FROM t;
+----------+
| COUNT(*) |
+----------+
|    10000 |
+----------+
1 row in set (0.00 sec)

從B+樹中定位記錄

我們現在想執行下邊這個語句:

SELECT * FROM t WHERE key1 >= 'b' AND key1 <= 'c';

假設優化器選擇使用二級索引來執行查詢,那麼查詢語句的執行示意圖就如下圖所示:

小貼士:原諒我把索引對應的複雜的B+樹結構搞了一個極度精簡版,為了突出重點,我們忽略掉了頁的結構,直接把所有的葉子節點的記錄都放在一起展示。我們想突出的重點就是:B+樹葉子節點中的記錄是按照索引列值大小排序的,對於的聚簇索引來說,它對應的B+樹葉子節點中的記錄就是按照id列排序的,對於idx_key1二級索引來說,它對應的B+樹葉子節點中的記錄就是按照key1列排序的。

我們想查詢key1列的值在['b', 'c']這個區間中的記錄,那麼就需要:

  • 先通過idx_key1索引對應的B+樹快速定位到key1列值為'b'、並且最靠左的那條二級索引記錄,該二級索引記錄中包含著對應的主鍵值,根據這個主鍵值再到聚簇索引中定位到完整的記錄(這個過程稱之為回表),將其返回給server層,server層再發送給客戶端。
  • 記錄按照鍵值由小到大的順序排列成一個單鏈表的形式,所以我們可以沿著這個單鏈表接著定位到下一條二級索引記錄,並且執行回表操作,將完整的記錄交給server層之後傳送給客戶端。
  • 繼續沿著記錄的單向連結串列查詢,重複上述過程,直到找到的二級索引記錄的key1列的值不滿足key1 <= 'c'
    的這個條件,如圖所示,也就是當我們在idx_key1二級索引中找到了key1='ca'的那條記錄後,發現它不符合key1 <= 'c'的條件,所以就停止查詢。

上述過程就是通過B+樹查詢一個鍵值在某一個範圍區間的記錄的過程。

包含IN子句的執行過程

如果我們想執行下邊這個語句:

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+樹定位記錄的次數就越多。

IN子句中引數值重複的情況

比方說下邊這條語句:

SELECT * FROM t WHERE key1 IN ('b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b');

雖然IN子句中包含好多個引數,但MySQL在語法解析的時候只會為其生成一個範圍區間,那就是:['b', 'b']

IN子句的引數順序問題

比方說下邊這條語句:

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']這個範圍區間中的記錄。

系統變數eq_range_index_dive_limit對IN子句的影響

大家一定要記著:MySQL優化器決定使用某個索引執行查詢的僅僅是因為:使用該索引時的成本足夠低。也就是說即使我們有下邊的語句:

SELECT * FROM t WHERE  key1 IN ('b', 'c');

MySQL優化器需要去分析一下如果使用二級索引idx_key1執行查詢的話,鍵值在['b', 'b']['c', 'c']這兩個範圍區間的記錄共有多少條,然後通過一定方式計算出成本,與全表掃描的成本相對比,選取成本更低的那種方式執行查詢。

在計算查詢成本的這一步驟中大家需要注意,對於包含IN子句條件的查詢來說,需要依次分析一下每一個範圍區間中的記錄數量是多少。MySQL優化器針對IN子句對應的範圍區間的多少而指定了不同的策略:

  • 如果IN子句對應的範圍區間比較少,那麼將率先去訪問一下儲存引擎,看一下每個範圍區間中的記錄有多少條(如果範圍區間的記錄比較少,那麼統計結果就是精確的,反之會採用一定的手段計算一個模糊的值,當然演算法也比較麻煩,我們就不展開說了,小冊裡有說),這種在查詢真正執行前優化器就率先訪問索引來計算需要掃描的索引記錄數量的方式稱之為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 值太小導致的。