in/or到底能不能用索引應該是肯定的,但有時生效有時不生效,這個能不能量化計算?這是本文想討論和解答的問題。
in到底用不用索引感覺像一樁懸疑片!古早時期的面經,統一說不走索引,在一些程式設計師腦海中從此留下不可磨滅的印記。
有些從業時間較長的程式設計師腦子裡的第一反應就是不走索引,上個月我就曾經被同事這樣質疑過。
但是那是mysql5.5以前的老黃曆了,現在都到8.0+了,5.5(甚至更早)以後可以肯定的是它會走索引。
但必然走索引嗎?不一定。
我搜尋引擎上搜尋關鍵詞 in/or
和 索引
,出來一大片文章,一般都會說,in/or能走索引,但後面跟的條件個數多了就不走索引了。
但問題就來了,這個多了
到底是多少才算多?
對於一個動態查詢的SQL,我咋知道到底走不走索引?
如何量化計算呢?
這時候就語焉不詳或者直接跳過。
大名鼎鼎的《阿里巴巴JAVA開發手冊》倒是一刀切。
最好不超過1000。
人家這規範只是推薦,也不是強制,是吧,不能吐槽。
而且超過1000就算用上了range級別的查詢,那可能也快不到哪裡去啊,對於要求快速響應的網際網路需求來說這推薦好像沒毛病。
但這不是重點,今天的重點在於,我一定要搞清楚,在保證explain
的type為range
而不是ALL
全表掃描的前提下,到底select * from table where id in (1,2,3.....x)
這個x
能到多少。
首先建一張測試表,來一步復現一下,走與不走索引的情況。
mysql
版本:5.7.19
引擎:innodb
建立一個測試表
CREATE TABLE `t_person` (
`id` int(11) NOT NULL,
`name` varchar(10) COLLATE utf8_bin DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
使用SQL
EXPLAIN SELECT id, NAME FROM t_person WHERE id IN (1)
檢視執行計劃
此時表裡無資料,顯示的是no matching row in const table
.
少量資料
插入一條資料insert t_person (id,name) values(1,'張三')
使用SQL
EXPLAIN SELECT id, NAME FROM t_person WHERE id IN (1)
檢視執行計劃
使用了索引,還是效率最高的const
(system生產環境不可能的吧),此時id in(1)
相當於 id = 1
。
在in裡增加點條件。
sql變成EXPLAIN SELECT id, NAME FROM t_person WHERE id IN (1, 2)
檢視執行計劃
使用了索引,但級別下降到了range
,即範圍索引。
繼續在in裡增加條件。
sql變成EXPLAIN SELECT id, NAME FROM t_person WHERE id IN (1, 2,3)
檢視執行計劃
索引級別變成了ALL
,即全表掃描,其實是索引失效了。
再往表裡插入兩條資料。此時總共3條資料。
insert t_person (id,name) values(2,'李四')
insert t_person (id,name) values(3,'王五')
再使用sqlEXPLAIN SELECT id, NAME FROM t_person WHERE id IN (1, 2,3)
檢視執行計劃
可以看到,隨時表資料的增加,同樣的sql執行計劃從ALL
變回了range
,索引又生效了。
同樣地,再增加一個in條件,EXPLAIN SELECT id, NAME FROM t_person WHERE id IN (1,2,3,4)
的執行計劃又變回了ALL
,這裡就不放圖了。
多點資料
以上只是小打小鬧撒撒水啦,總共幾條資料,in的條件都快超過表資料了,執行計算都不用預估就知道全表掃描還好一點啦。
我再往表裡插入100萬條資料。
我先按照阿里的開發規範推薦的1000這個值作為臨界值,先使用900個條件
再使用1100個條件
上圖表明,這兩種情況都使用到了range範圍索引呢。
再加大劑量,直接上10萬。
步子邁大了,咔,這下終於全表掃描了。
但是還是沒找到臨界值。
https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html
我在這裡尋找到了一個引數,描述的倒像是相似的問題。
這個方法說的是當使用in或or查詢時,比如where in(1,2,3),執行引擎會先預估表中的數量,表中的數量將決定使用的查詢方式,比如,如果表中只有3條資料,那麼很明顯,這時候直接全表掃描。
而這個預估的方法有2種,一是dive到index中即利用索引完成元組數的估算,簡稱index dive; 二是使用索引的統計數值,進行估算.
相比這2種方式,在效果上:
index dive: 速度慢,但能得到精確的值(MySQL的實現是數索引對應的索引項個數,所以精確)
index statistics: 速度快,但得到的值未必精確.
但eq_range_index_dive_limit
這個引數確實跟今天的主題相關係數不大。很明顯,這個值在mysql 5.7是200, 一開始的in後面的條件個數就是900,依然是走了range索引的。
於是我找到了stackoverflow,在上面把msyql
in
count
這些關鍵詞搜了一下,沒有找到相關的問題。
然後我把問題詳細描述了一下,提了一個新的問題,沒想到啊,半個小時不到,人家就直接給我點踩,並給出了相似的已解答問題。
尷尬了。
我超喜歡stackoverflow,這裡的人個個都是人才。
相似的問題在這裡。
https://stackoverflow.com/questions/72361880/mysql-in-operator-on-large-number-of-values
這位仁兄也在in的使用中也有很多問號,in的條件卡在14000左右,超過就失去了range索引。
下面高贊答案提到了一個引數,range_optimizer_max_mem_size
,一看就很有搞頭啊。
轉到mysql官網,憑我的渣渣英語也能看明白,我知道,大概我找到答案了。
https://dev.mysql.com/doc/refman/5.7/en/range-optimization.html
要控制範圍優化器可用的記憶體,使用range_optimizer_max_mem_size系統變數:
值為0表示「沒有限制」。
當值大於0時,優化器將跟蹤在考慮範圍存取方法時所消耗的記憶體。如果即將超過指定的限制,則放棄範圍存取方法,轉而考慮其他方法,包括全表掃描
。這可能不太理想。如果發生這種情況,會出現以下警告(其中N是當前的range_optimizer_max_mem_size值)。
現在事情就很簡單了。
range_optimizer_max_mem_size預設是8M,使用同樣的SQL,in後面同樣的條件為固定的19900個,
在range_optimizer_max_mem_size=8M
,range_optimizer_max_mem_size=8
情況下分別執行一下看效果。
range_optimizer_max_mem_size=8M時,走range索引。
range_optimizer_max_mem_size=8時,走ALL全表掃描。
破案了!
明明官網上就有答案,我卻三過家門而不入。
in兩種情況會走全表掃描。
推而廣之,or也是一樣的道理。
歸根結底都是範圍查詢。
當然,總體來說,in後面條件越少越好,假設一張表有1000萬條資料,in後面的條件有10000個,這時候就算走了range索引,估計效率也好不到哪裡。