導讀
你還記得我在《字段為NULL會影響查詢性能嗎?》這一章中遺留的一個懸念嗎?如果不記得了可以回到這個章節,搜索關鍵字“IN字段查詢多少個值最合適?”可以找到。
現在我們再回顧一下這個懸念,也就是查找輔助索引index_birthday得到主鍵6,8,2,5,這4個主鍵是如何到聚簇索引查找完整記錄的?乍一看,這個不就是SQL中Where條件為id IN (6,8,2,5)這樣的查詢嘛!其實不然,MySQL對這兩者查詢處理的方式是完全不同的。所以,在這一章節中,我將分別講解MySQL對這兩者的查詢是如何處理的。
先來看SQL中Where條件為id IN (6,8,2,5)這樣的查詢。
我們都知道MySQL優化器的目的是針對一條SQL,確定其最優的查詢執行計划,那么,要產生這個最優的執行計划,勢必要對該條SQL做一個詳細的分析,那么,這樣一個分析的過程,在MySQL內部,就叫做成本代價模型分析。而對於IN查詢語句,其實就是問在MySQL內部,IN查詢語句,它的查詢成本是如何分析的,通俗的講就是某一個字段的IN查詢條件里到底寫多個值,MySQL能夠選擇出最高效的執行計划?咦?這不就是這一章節標題問的那個問題嗎?所以,我將結合id IN (6,8,2,5)這條查詢,重點講解一下IN查詢語句的成本分析的過程,然后來回答本章標題的這個問題。
成本分析
方便理解,我先把id IN (6,8,2,5)這條查詢轉化為下面這條完整的SQL:
SELECT * FROM user WHERE id IN (6, 8, 2, 5)
在MySQL中有一個配置參數eq_range_index_dive_limit,它的作用是一個等值查詢(比如:in查詢),其等值條件數小於該配置參數,則查詢成本分析使用掃描索引樹的方式分析,如果大於等於該配置參數,則使用索引統計的方式分析。使用掃描索引樹的方式分析在MySQL內部叫做index dives,使用索引統計的方式分析在MySQL內部叫做index statistics。
結合上面這條SQL,就是如果SQL中IN查詢字段id的值出現的數量小於eq_range_index_dive_limit,則走索引樹掃描分析查詢成本,大於等於eq_range_index_dive_limit,則走索引統計的方式分析查詢成本。
掃描索引樹的方式分析SQL的查詢成本,它的好處就是在IN查詢的值數量不多時,得到的成本結果是精確的,這就意味着MySQL可以選擇正確的執行計划,保證語句查詢的性能。你現在一定有個疑問:為什么說是在IN查詢的值數量不多時才是精確的,因為掃描性能的原因,MySQL在IN查詢的值數量很多的情況下,掃描索引樹成本提高,性能下降,導致查詢成本分析代價也隨之提高了。
而基於索引統計的方式分析SQL的查詢成本,由於無需掃描索引樹,所以,它的優勢就是查詢成本分析過程快,代價低。但是,它的缺點也很明顯,由於無需掃描索引樹,通過粗略統計索引使用情況,得出查詢成本,導致MySQL可能選錯執行計划,使得SQL查詢性能下降。
關於查詢成本分析的兩個方案:掃描索引樹 和 索引統計 兩種方式,我將在《MySQL為什么選擇執行計划A而不選擇B?》這一章節中詳細講解。
綜合兩種成本分析的方案,我們一般讓MySQL在分析一條SQL時,盡可能使用掃描索引樹的方案分析查詢成本,然后,正確選擇執行計划,最終,保證該條SQL的查詢性能。
所以,本章標題的那個問題,即IN字段查詢多少個值最合適?結合上面的分析,我的回答是IN查詢的字段,該字段的值不要超過eq_range_index_dive_limit這個參數,讓MySQL能夠正確選擇執行計划,保證SQL查詢的性能。 eq_range_index_dive_limit參數的默認值在5.7版本更新為200。
我們假設MySQL明確選擇了一個最優的執行計划去執行上面這條SQL,那么,這個執行計划又是怎么執行的呢?這個我將在《MySQL執行器比我們想象的要聰明!》這一章節中詳細講解。
參數配置
關於eq_range_index_dive_limit這個參數如何查看,我們可以使用下面這條SQL查看:
SHOW VARIABLES LIKE '%dive%';
現在,我們再來看本章《導讀》的另一個問題,即查找輔助索引index_birthday得到主鍵6,8,2,5,這4個主鍵是如何到聚簇索引查找完整記錄的?
MRR
聰明的MySQL在處理上面這個問題時,想到了一個把隨機IO轉換為順序IO的辦法,去提升輔助索引主鍵到聚簇索引查詢的性能,而這個辦法就是MRR。下面我就結合上面這個問題,來詳細講解一下這個MRR。

如上圖,我簡化了輔助索引和聚簇索引的結構,只保留了葉子節點:
- 優化器先遍歷輔助索引index_birthday中的葉子節點頁6和頁7,按順序讀取節點內每個記錄如下:
(1) 讀取頁6中第二條記錄的主鍵6,將6放到一個叫做rowids_buf的緩沖區。
(2) 讀取頁7中第一條記錄的主鍵8,將8放到rowids_buf的緩沖區。
(3) 讀取頁7中第二條記錄的主鍵2,將2放到rowids_buf的緩沖區。
(4) 讀取頁7中第三條記錄的主鍵5,將5放到rowids_buf的緩沖區。
- 優化器對rowids_buf中的6,8,2,5這四個主鍵做快速排序,即圖中qsort,然后,rowids_buf中的主鍵順序變為2,5,6,8。
- 執行器從rowids_buf中按序依次取出主鍵2,5,6,8,圖中虛線箭頭部分,然后,進行如下步驟:
(1) 根據主鍵2,順序查找聚簇索引葉子節點鏈表,即頁4 ~ 頁7。
(2) 在頁4中找到主鍵為2的記錄,start_cusor游標定位到主鍵為2的記錄上。
(3) 根據主鍵5,從start_cusor游標位置開始,順序查找聚簇索引葉子節點頁5 ~ 頁7。
(4) 在頁6中找到主鍵為5的記錄,start_cusor游標定位到主鍵為5的記錄上。
(5) 以此類推,最后在聚簇索引葉子節點上找到了主鍵6和8的兩條記錄。
相比原先按照主鍵6,8,2,5的順序搜索聚簇索引,MRR的做法要聰明多了,因為按照6,8,2,5這個順序搜索聚簇索引,這是一個隨機查找,而轉換為2,5,6,8后,搜索聚簇索引就變為順序查找,這個性能相比前者一定會有很大提升的。
這時,你可能有個疑問:如果rowids_buf滿了怎么辦?這個不用擔心,當rowids_buf滿了,MySQL會先執行上面的步驟2和3,等rowids_buf中的主鍵都從聚簇索引中取出記錄后,把rowids_buf清空,繼續開始步驟1 ~ 3。
既然rowids_buf會滿,我想一次多加載一批輔助索引主鍵到rowids_buf,減少查找聚簇索引的次數,那該怎么辦?這個MySQL也提供了一個參數去改變rowids_buf的大小,這個參數就是read_rnd_buffer_size。調整語句:
SET read_rnd_buffer_size = 1024*1024;
小結
最后,我來總結一下這一章節所講的內容:
- IN查詢的成本分析: IN查詢字段值的個數受
eq_range_index_dive_limit這個參數影響,所以,建議這個個數不要超過該參數所配置的大小。 - MRR: 一個將隨機查找變為順序查找的好辦法,提升了輔助索引主鍵到聚簇索引查找的性能。為了減少查找聚簇索引的次數,可以調整參數
read_rnd_buffer_size。
