在MySQL中一張表實際上是能夠支持多個索引的。可是,你寫SQL語句的時候,並無主動指定使用哪一個索引。也就是說,使用哪一個索引是由MySQL來肯定的。mysql
通常在數據庫使用的時候回遇到這樣的問題,一條原本能夠執行很快的語句,卻因爲MySQL選錯了索引,致使執行速度變得很慢。程序員
舉例說明:sql
咱們先建一個簡單的表,表裏有a、b兩個字段,並分別建上索引:數據庫
CREATE TABLE `t` (session
`id` int(11) NOT NULL,測試
`a` int(11) DEFAULT NULL,優化
`b` int(11) DEFAULT NULL,線程
PRIMARY KEY (`id`),3d
KEY `a` (`a`),日誌
KEY `b` (`b`)
) ENGINE=InnoDB;
而後,咱們往表t中插入10萬行記錄,取值按整數遞增,即:(1,1,1),(2,2,2),(3,3,3) 直到(100000,100000,100000)。
是用存儲過程來插入數據的:
delimiter ;;
create procedure idata()
begin
declare i int;
set i=1;
while(i<=100000)do
insert into t values(i, i, i);
set i=i+1;
end while;
end;;
delimiter ;
call idata();
接下來,咱們分析一條SQL語句:
mysql> select * from t where a between 10000 and 20000;
必定會說,這個語句還用分析嗎,很簡單呀,a上有索引,確定是要使用索引a的。
圖1顯示的就是使用explain命令看到的這條語句的執行狀況。
圖1看上去,這條查詢語句的執行也確實符合預期,key這個字段值是’a’,表示優化器選擇了索引a。
不過別急,這個案例不會這麼簡單。在咱們已經準備好的包含了10萬行數據的表上,咱們再作以下操做。
這時候,session B的查詢語句select * from t where a between 10000 and 20000就不會再選擇索引a了。咱們能夠經過慢查詢日誌(slow log)來查看一下具體的執行狀況。
爲了說明優化器選擇的結果是否正確,我增長了一個對照,即:使用force index(a)來讓優化器強制使用索引a(這部份內容,我還會在這篇文章的後半部分中提到)。
下面的三條SQL語句,就是這個實驗過程。
set long_query_time=0;
select * from t where a between 10000 and 20000; /*Q1*/
select * from t force index(a) where a between 10000 and 20000;/*Q2*/
第一句,是將慢查詢日誌的閾值設置爲0,表示這個線程接下來的語句都會被記錄入慢查詢日誌中;
第二句,Q1是session B原來的查詢;
第三句,Q2是加了force index(a)來和session B原來的查詢語句執行狀況對比。
如圖3所示是這三條SQL語句執行完成後的慢查詢日誌。
能夠看到,Q1掃描了10萬行,顯然是走了全表掃描,執行時間是40毫秒。Q2掃描了10001行,執行了21毫秒。也就是說,咱們在沒有使用force index的時候,MySQL用錯了索引,致使了更長的執行時間。
這個例子對應的是咱們日常不斷地刪除歷史數據和新增數據的場景。這時,MySQL居然會選錯索引,是否是有點奇怪呢?
選擇索引是優化器的工做。
而優化器選擇索引的目的,是找到一個最優的執行方案,並用最小的代價去執行語句。在數據庫裏面,掃描行數是影響執行代價的因素之一。掃描的行數越少,意味着訪問磁盤數據的次數越少,消耗的CPU資源越少。
固然,掃描行數並非惟一的判斷標準,優化器還會結合是否使用臨時表、是否排序等因素進行綜合判斷。
咱們這個簡單的查詢語句並無涉及到臨時表和排序,因此MySQL選錯索引確定是在判斷掃描行數的時候出問題了。
問題是怎麼掃描的行數呢?
MySQL在真正開始執行語句以前,並不能精確地知道知足這個條件的記錄有多少條,而只能根據統計信息來估算記錄數。
這個統計信息就是索引的「區分度」。顯然,一個索引上不一樣的值越多,這個索引的區分度就越好。而一個索引上不一樣的值的個數,咱們稱之爲「基數」(cardinality)。也就是說,這個基數越大,索引的區分度越好。
咱們可使用show index方法,看到一個索引的基數。如圖4所示,就是表t的show index 的結果 。雖然這個表的每一行的三個字段值都是同樣的,可是在統計信息中,這三個索引的基數值並不一樣,並且其實都不許確。
爲何要採樣統計呢?由於把整張表取出來一行行統計,雖然能夠獲得精確的結果,可是代價過高了,因此只能選擇「採樣統計」。
採樣統計的時候,InnoDB默認會選擇N個數據頁,統計這些頁面上的不一樣值,獲得一個平均值,而後乘以這個索引的頁面數,就獲得了這個索引的基數。
而數據表是會持續更新的,索引統計信息也不會固定不變。因此,當變動的數據行數超過1/M的時候,會自動觸發從新作一次索引統計。
在MySQL中,有兩種存儲索引統計的方式,能夠經過設置參數innodb_stats_persistent的值來選擇:
設置爲on的時候,表示統計信息會持久化存儲。這時,默認的N是20,M是10。
設置爲off的時候,表示統計信息只存儲在內存中。這時,默認的N是8,M是16。
因爲是採樣統計,因此無論N是20仍是8,這個基數都是很容易不許的。
但,這還不是所有。
能夠從圖4中看到,此次的索引統計值(cardinality列)雖然不夠精確,但大致上仍是差很少的,選錯索引必定還有別的緣由。
其實索引統計只是一個輸入,對於一個具體的語句來講,優化器還要判斷,執行這個語句自己要掃描多少行。
接下來,咱們再一塊兒看看優化器預估的,這兩個語句的掃描行數是多少。
rows這個字段表示的是預計掃描行數。
其中,Q1的結果仍是符合預期的,rows的值是104620;可是Q2的rows值是37116,誤差就大了。而圖1中咱們用explain命令看到的rows是隻有10001行,是這個誤差誤導了優化器的判斷。
到這裏,可能你的第一個疑問不是爲何不許,而是優化器爲何放着掃描37000行的執行計劃不用,卻選擇了掃描行數是100000的執行計劃呢?
這是由於,若是使用索引a,每次從索引a上拿到一個值,都要回到主鍵索引上查出整行數據,這個代價優化器也要算進去的。
而若是選擇掃描10萬行,是直接在主鍵索引上掃描的,沒有額外的代價。
優化器會估算這兩個選擇的代價,從結果看來,優化器認爲直接掃描主鍵索引更快。固然,從執行時間看來,這個選擇並非最優的。
使用普通索引須要把回表的代價算進去,在圖1執行explain的時候,也考慮了這個策略的代價 ,但圖1的選擇是對的。也就是說,這個策略並無問題。
因此冤有頭債有主,MySQL選錯索引,這件事兒還得歸咎到沒能準確地判斷出掃描行數。
既然是統計信息不對,那就修正。analyze table t 命令,能夠用來從新統計索引信息。咱們來看一下執行效果。
因此在實踐中,若是你發現explain的結果預估的rows值跟實際狀況差距比較大,能夠採用這個方法來處理。
其實,若是隻是索引統計不許確,經過analyze命令能夠解決不少問題,可是前面咱們說了,優化器可不止是看掃描行數。
依然是基於這個表t,咱們看看另一個語句:
mysql> select * from t where (a between 1 and 1000) and (b between 50000 and 100000) order by b limit 1;
從條件上看,這個查詢沒有符合條件的記錄,所以會返回空集合。
在開始執行這條語句以前,你能夠先設想一下,若是你來選擇索引,會選擇哪個呢?
爲了便於分析,咱們先來看一下a、b這兩個索引的結構圖。
若是使用索引a進行查詢,那麼就是掃描索引a的前1000個值,而後取到對應的id,再到主鍵索引上去查出每一行,而後根據字段b來過濾。顯然這樣須要掃描1000行。
若是使用索引b進行查詢,那麼就是掃描索引b的最後50001個值,與上面的執行過程相同,也是須要回到主鍵索引上取值再判斷,因此須要掃描50001行。
因此你必定會想,若是使用索引a的話,執行速度明顯會快不少。那麼,下面咱們就來看看究竟是不是這麼一回事兒。
圖8是執行explain的結果。
mysql> explain select * from t where (a between 1 and 1000) and (b between 50000 and 100000) order by b limit 1;
能夠看到,返回結果中key字段顯示,此次優化器選擇了索引b,而rows字段顯示須要掃描的行數是50198。
從這個結果中,你能夠獲得兩個結論:
其實大多數時候優化器都能找到正確的索引,但偶爾仍是會碰到咱們舉例的這兩種狀況:本來能夠執行得很快的SQL語句,執行速度卻比預期的慢不少,該怎麼處理呢?
(1) 第一種方法採用force index強行選擇一個索引。MySQL會根據詞法解析的結果分析出可能可使用的索引做爲候選項,而後在候選列表中依次判斷每一個索引須要掃描多少行。若是force index指定的索引在候選索引列表中,就直接選擇這個索引,再也不評估其餘索引的執行代價。
不過不少程序員不喜歡使用force index,一來這麼寫不優美,二來若是索引改了名字,這個語句也得改,顯得很麻煩。並且若是之後遷移到別的數據庫的話,這個語法還可能會不兼容。
但其實使用force index最主要的問題仍是變動的及時性。由於選錯索引的狀況仍是比較少出現的,因此開發的時候一般不會先寫上force index。而是等到線上出現問題的時候,你纔會再去修改SQL語句、加上force index。可是修改以後還要測試和發佈,對於生產系統來講,這個過程不夠敏捷。
因此,數據庫的問題最好仍是在數據庫內部來解決。那麼,在數據庫裏面該怎樣解決呢?
(2) 第二種方法咱們能夠考慮修改語句,引導MySQL使用咱們指望的索引。
好比,在這個例子裏,顯然把「order by b limit 1」 改爲 「order by b,a limit 1」 ,語義的邏輯是相同的。
以前優化器選擇使用索引b,是由於它認爲使用索引b能夠避免排序(b自己是索引,已是有序的了,若是選擇索引b的話,不須要再作排序,只須要遍歷),因此即便掃描行數多,也斷定爲代價更小。
如今order by b,a 這種寫法,要求按照b,a排序,就意味着使用這兩個索引都須要排序。所以,掃描行數成了影響決策的主要條件,因而此時優化器選了只須要掃描1000行的索引a。
固然,這種修改並非通用的優化手段,只是恰好在這個語句裏面有limit 1,所以若是有知足條件的記錄, order by b limit 1和order by b,a limit 1 都會返回b是最小的那一行,邏輯上一致,才能夠這麼作。
若是你以爲修改語義這件事兒不太好,這裏還有一種改法,下圖是執行效果。
mysql> select * from (select * from t where (a between 1 and 1000) and (b between 50000 and 100000) order by b limit 100)alias limit 1;
在這個例子裏,咱們用limit 100讓優化器意識到,使用b索引代價是很高的。實際上是咱們根據數據特徵誘導了一下優化器,也不具有通用性。
(1) 第三種方法是,在有些場景下,咱們能夠新建一個更合適的索引,來提供給優化器作選擇,或刪掉誤用的索引。
不過,在這個例子中,我沒有找到經過新增索引來改變優化器行爲的方法。這種狀況其實比較少,尤爲是通過DBA索引優化過的庫,再碰到這個bug,找到一個更合適的索引通常比較難。
若是我說還有一個方法是刪掉索引b,你可能會以爲可笑。但實際上我碰到過兩次這樣的例子,最終是DBA跟業務開發溝通後,發現這個優化器錯誤選擇的索引其實根本沒有必要存在,因而就刪掉了這個索引,優化器也就從新選擇到了正確的索引。
索引統計的更新機制,並提到了優化器存在選錯索引的可能性。
對於因爲索引統計信息不許確致使的問題,你能夠用analyze table來解決。
而對於其餘優化器誤判的狀況,你能夠在應用端用force index來強行指定索引,也能夠經過修改語句來引導優化器,還能夠經過增長或者刪除索引來繞過這個問題。