2. mysql 為什么會選錯索引

上節(jié)我們講了mysql的整體架構(gòu)苹支,知道了優(yōu)化器的作用是優(yōu)化sql嗜历,選擇索引,生成執(zhí)行計劃吨铸。索引是優(yōu)化器階段自己選擇的行拢,優(yōu)化器大部分情況下索引的選擇都是比較合理的,但是也有特殊情況下诞吱,優(yōu)化器選擇的索引并不是最優(yōu)的舟奠。下面我們通過實驗來說明這個問題竭缝,并給出優(yōu)化方案(需要說明的是本實驗的環(huán)境是mysql版本是5.7,事務隔離級別是RR,事務自動提交)。

首先我們來建一個表沼瘫,表里面有id抬纸、a、b三個字段耿戚,下面是建表語句

CREATE TABLE `t` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `a` int(11) DEFAULT NULL,
  `b` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  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(a,b) values(i, i);
    set i=i+1;
  end while;
end;;
delimiter ;
call idata();

這里要注意的是我的mysql默認是自動提交飞几,且sync_binlog=1,innodb_flush_log_at_trx_commit=1砚哆,所以說上面的存儲過程每次插入數(shù)據(jù),在提交的時候binlog和redolog都會寫到磁盤屑墨,這大大增加了IO交互躁锁,導致整個插入過程非常慢,在我的電腦上運行了30分鐘才插入完成卵史,解決方法有很多可以設(shè)置sync_binlog=0,innodb_flush_log_at_trx_commit=0战转,sync_binlog=0表示每次提交只write(從binlog cache 寫到page cache)不fsync(寫到磁盤),innodb_flush_log_at_trx_commit=0表示每次提交只寫到redo log buffer(就是不做任何操作以躯,因為redo log最開始生成的時候就在redo log buffer 中槐秧,Innodb 后臺有一個線程,每1s就會將redo log buffer里面的數(shù)據(jù)調(diào)用write寫到page cache 然后再調(diào)用 fsync 寫到磁盤)忧设,還需注意的是binlog cache 是線程私有的刁标,redo log buffer是公共的,所以處以prepare階段的redo log是可能被持久化的址晕。

插入數(shù)據(jù)后膀懈,我們分析一下sql 語句的執(zhí)行計劃

EXPLAIN SELECT * from t where a BETWEEN 10000 and 20000;

你肯定會想這還不簡單嗎?字段a上有索引肯定走的索引a,事實也是這樣的谨垃,下面是執(zhí)行計劃的截圖


正常索引.png

我們再做如下操作启搂,看看情況怎樣

sessionA sessionB
start transaction with consistent snapshot;
delete from t;
call idata();
EXPLAIN SELECT * from t where a BETWEEN 10000 and 20000;
commit;

start transaction with consistent snapshot; 代表的意思是執(zhí)行這句話就啟動了事務,就生成一致性視圖刘陶,如果只是start transaction 只有在第一次查詢的時候才會生成一致性視圖胳赌,需要說明的是start transaction并不是事務的起點,在執(zhí)行到start transaction之后的第一個操作才啟動事務匙隔,第一個查詢語句生成的trx_id 是虛假的疑苫,是為顯示用的,只有事務性操作生成的trx_id 才會存于一致性視圖數(shù)組中。這個一致性視圖數(shù)組是mvcc實現(xiàn)的基礎(chǔ)缀匕。

然后纳决,我們現(xiàn)在再來看一下sessionB 的 EXPLAIN SELECT * from t where a BETWEEN 10000 and 20000;

有誤的索引.png

發(fā)現(xiàn)此時是全表掃描,key處為空乡小,rows為100015行阔加,沒有走索引a,和我們預想的可能不太一樣满钟,為了更準確的說明胜榔,是mysql選錯了索引,我們再來做個對比湃番。

set GLOBAL slow_query_log = 1;

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**/
  • 第一條語句是打開慢日志的開關(guān) 夭织,這是個全局的開關(guān)
  • 第二條語句是慢日志的標準,就是說什么樣的語句是慢日志吠撮,才會被慢日志記錄下來尊惰。這里的意思是只要操作時間超過0s就會記錄下來,所以這里是記錄所有的操作語句(增刪改查都會記錄)泥兰。
  • 第三條是在sessionB下的查詢語句(Q1)
  • 第四條是在sessionB下的查詢語句(Q2)弄屡,但是強制使用的索引a

我們來看看Q1與Q2在慢日志中的具體查詢信息

# Time: 2021-05-03T02:17:03.047811Z
# User@Host: root[root] @ localhost [::1]  Id:    20
# Query_time: 0.035942  Lock_time: 0.000000 Rows_sent: 10001  Rows_examined: 100000
SET timestamp=1620008223;
SELECT * from t where a BETWEEN 10000 and 20000;

# Time: 2021-05-03T02:18:08.055825Z
# User@Host: root[root] @ localhost [::1]  Id:    20
# Query_time: 0.021942  Lock_time: 0.000000 Rows_sent: 10001  Rows_examined: 10001
SET timestamp=1620008288;
SELECT * from t FORCE INDEX(a) where a BETWEEN 10000 and 20000;

可以看到?jīng)]有使用強制索引a的Q1語句確實是全表掃描了Rows_examined: 100000,Query_time: 0.035942,強制使用了索引a的Q2語句Rows_examined: 10001,Query_time: 0.021942,到這里我們可以肯定說鞋诗,mysql沒有使用最優(yōu)的方案來查詢膀捷,下面我們就來具體分析分析為什么!

首先我們要明白的是優(yōu)化器是怎么選擇索引的削彬,索引的選擇有很多因素影響包括是否排序全庸、是否使用到臨時表、掃描行數(shù)等因素融痛,優(yōu)化器是綜合考慮各個因素選擇的最優(yōu)方案壶笼,這里我們的語句SELECT * from t where a BETWEEN 10000 and 20000;,沒有使用到臨時表雁刷,也沒有排序拌消,所以這里最關(guān)鍵的影響就是掃描行數(shù),掃描行數(shù)越少安券,訪問磁盤的次數(shù)越少,需要消耗的CPU資源就越少氓英。接下來問題又來了侯勉,掃描行數(shù)是怎么判斷的呢?

我們在優(yōu)化器階段就判斷了掃描的行數(shù)(執(zhí)行計劃中的Rows)铝阐,但這個掃描的行數(shù)是預估的址貌,不是準確的,為什么要預估呢?因為掃描所有數(shù)據(jù)需要消耗大量的資源练对,比如CPU等等遍蟋,那是怎么預估的呢。這里有個基數(shù)的概念螟凭,基數(shù)是什么呢虚青?基數(shù)是一個索引上不同值的個數(shù),索引的基數(shù)越大說明索引的區(qū)分度越好螺男,掃描的行數(shù)可能就越少棒厘,索引的基數(shù)不是實時變化的,它也是一個統(tǒng)計的結(jié)果下隧,那它是怎么統(tǒng)計的呢奢人?

show index from t 來查看表t上所有索引的基數(shù)

基數(shù)的統(tǒng)計 :基數(shù)的統(tǒng)計是采用統(tǒng)計的方法計算的,在統(tǒng)計的時候默認隨機取N頁數(shù)據(jù)淆院,統(tǒng)計這N頁數(shù)據(jù)所有不同的數(shù)值的個數(shù)得到平均值何乎,然后再乘以這個索引的總頁數(shù),最后得到基數(shù)土辩。數(shù)據(jù)表里面的數(shù)據(jù)在不斷變更支救,當變更的數(shù)據(jù)超過總行數(shù)的1/M時會再次觸發(fā)統(tǒng)計,修正原先統(tǒng)計的基數(shù)脯燃。至于M和N 到底是多少搂妻?在mysql中有兩種存儲索引的方式,可以通過設(shè)置參數(shù)innodb_stats_persistent 的值來選擇:

  • 設(shè)置為 on 的時候辕棚,表示統(tǒng)計信息會持久化存儲欲主。這時,默認的 N 是 20逝嚎,M 是 10扁瓢。

  • 設(shè)置為 off 的時候,表示統(tǒng)計信息只存儲在內(nèi)存中补君。這時引几,默認的 N 是 8,M 是 16

通過基數(shù)挽铁,優(yōu)化器可以判斷會不會選擇該索引(這里猜測有一個閾值)伟桅,如果區(qū)分度實在是太小則放棄該索引(這就是建議我們不要在區(qū)分度不高的列上加索引,因為沒啥用)叽掘,經(jīng)過第一層過濾楣铁,優(yōu)化器還要判斷掃描行數(shù),就是執(zhí)行計劃的rows更扁,這個rows也是一個統(tǒng)計的結(jié)果(根據(jù)where條件在索引上統(tǒng)計)盖腕,看rows是不是太大赫冬,如果太大,優(yōu)化器想我還不如直接全表掃描算了溃列,免得還要回表劲厌,所以這里還有一個掃描行數(shù)的權(quán)衡,到這里我們基本講完了索引是怎么選擇的了听隐。

回到本文的示例补鼻,sessionB中的查詢語句為什么不走索引a呢?那就是mysql的統(tǒng)計的rows太大遵绰,優(yōu)化器覺得使用索引a不劃算 辽幌。那為什么mysql的統(tǒng)計的rows太大,是因為 sessionA語句來了就start transaction with consistent snapshot; 椿访,這個語句一執(zhí)行就創(chuàng)建了視圖乌企,根據(jù)mvcc規(guī)則,它要看見它能看見的數(shù)據(jù)成玫,sessionB執(zhí)行刪除操作加酵,只是把每行的數(shù)據(jù)標記為已刪除,實際并沒有刪除(ibd文件大小并沒有發(fā)生改變)哭当,新插入的數(shù)據(jù)因為id是遞增的不能復用被標記刪除的位置猪腕,所以通過頁分裂的形式把新增的數(shù)據(jù)放入對應的數(shù)據(jù)頁中,這樣在統(tǒng)計rows的時候因為連帶被標記刪除的行一起統(tǒng)計钦勘,導致rows統(tǒng)計的行數(shù)偏大陋葡,從而進一步導致優(yōu)化器選擇了全表掃描。

這里要補充一下頁的知識:

  1. Innodb與磁盤交互的最小單位是頁彻采,所以說加載數(shù)據(jù)到內(nèi)存是按照頁來加載的而不是一行一行加載腐缤,索引只能定位到頁不能定位到具體哪一行,哪一行只能在頁中通過二分法來進行查找

  2. 頁與頁之間是通過雙向鏈表來鏈接的肛响,頁內(nèi)部的數(shù)據(jù)行是通過單向鏈表來鏈接的岭粤。

  3. 插入數(shù)據(jù)可能會導致頁的分裂,尤其是非自增主鍵特笋,從而導致頁空洞(就是頁的利用率不高)剃浇;刪除數(shù)據(jù)會導致數(shù)據(jù)的空洞,也可能會導致頁的合并

  4. 頁的結(jié)構(gòu) 如附件表所示

那我們知道了原因猎物,如何進行優(yōu)化呢虎囚。主要有以下幾種解決方法

  1. analyze table ,通過該命令重新統(tǒng)計基數(shù)信息
    analyze.png
  1. 通過 force index ,強制使用某個索引蔫磨,告訴優(yōu)化器你不用選擇索引了溜宽,我就用這個,但是該方案十分的不優(yōu)雅

    MySQL 會根據(jù)詞法解析的結(jié)果分析出可能可以使用的索引作為候選項质帅,我們經(jīng)常可以看到執(zhí)行計劃的possible_key有幾個候選項,然后在候選列表中依次判斷每個索引需要掃描多少行煤惩〖掂郑基本上是選擇掃描行數(shù)最少的作為最后的索引,如果 force index 指定的索引在候選索引列表中魄揉,就直接選擇這個索引剪侮,不再評估其他索引的執(zhí)行代價。

  2. 在不改變語義的情況下洛退,修改sql語句瓣俯,引導執(zhí)行計劃選擇我們想要的索引

  3. 新建一個更合適的索引或者刪除誤用或沒有的索引

小結(jié)

本篇文章我們具體分析了mysql是怎么選擇索引的,以及為什么會選錯索引兵怯,并給出了四種優(yōu)化方案彩匕,謝謝。

附件

頁結(jié)構(gòu)表:


頁結(jié)構(gòu).png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末媒区,一起剝皮案震驚了整個濱河市驼仪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌袜漩,老刑警劉巖绪爸,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異宙攻,居然都是意外死亡奠货,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進店門座掘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來递惋,“玉大人,你說我怎么就攤上這事雹顺〉つ” “怎么了?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵嬉愧,是天一觀的道長贩挣。 經(jīng)常有香客問我,道長没酣,這世上最難降的妖魔是什么王财? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮裕便,結(jié)果婚禮上绒净,老公的妹妹穿的比我還像新娘。我一直安慰自己偿衰,他們只是感情好挂疆,可當我...
    茶點故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布改览。 她就那樣靜靜地躺著,像睡著了一般缤言。 火紅的嫁衣襯著肌膚如雪宝当。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天胆萧,我揣著相機與錄音庆揩,去河邊找鬼。 笑死跌穗,一個胖子當著我的面吹牛订晌,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蚌吸,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼锈拨,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了套利?” 一聲冷哼從身側(cè)響起推励,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎肉迫,沒想到半個月后验辞,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡喊衫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年跌造,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片族购。...
    茶點故事閱讀 39,992評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡壳贪,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出寝杖,到底是詐尸還是另有隱情违施,我是刑警寧澤,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布瑟幕,位于F島的核電站磕蒲,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏只盹。R本人自食惡果不足惜辣往,卻給世界環(huán)境...
    茶點故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望殖卑。 院中可真熱鬧站削,春花似錦、人聲如沸孵稽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至园细,卻和暖如春扯键,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背珊肃。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留馅笙,地道東北人伦乔。 一個月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像董习,于是被迫代替她去往敵國和親烈和。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,947評論 2 355

推薦閱讀更多精彩內(nèi)容