一條SQL語句執(zhí)行得很慢的原因有哪些优训?
說實(shí)話朵你,這個(gè)問題可以涉及到 MySQL 的很多核心知識(shí),可以扯出一大堆揣非,就像要考你計(jì)算機(jī)網(wǎng)絡(luò)的知識(shí)時(shí)抡医,問你“輸入U(xiǎn)RL回車之后,究竟發(fā)生了什么”一樣早敬,看看你能說出多少了忌傻。
之前騰訊面試的實(shí)話,也問到這個(gè)問題了搞监,不過答的很不好水孩,之前沒去想過相關(guān)原因,導(dǎo)致一時(shí)之間扯不出來琐驴。所以今天俘种,我?guī)Т蠹襾碓敿?xì)扯一下有哪些原因,相信你看完之后一定會(huì)有所收獲绝淡,不然你打我宙刘。
一、分類討論
一條 SQL 語句執(zhí)行的很慢够委,那是每次執(zhí)行都很慢呢荐类?還是大多數(shù)情況下是正常的,偶爾出現(xiàn)很慢呢茁帽?所以我覺得玉罐,我們還得分以下兩種情況來討論。
1潘拨、大多數(shù)情況是正常的吊输,只是偶爾會(huì)出現(xiàn)很慢的情況。
2铁追、在數(shù)據(jù)量不變的情況下季蚂,這條SQL語句一直以來都執(zhí)行的很慢。
針對(duì)這兩種情況琅束,我們來分析下可能是哪些原因?qū)е碌摹?/p>
二扭屁、針對(duì)偶爾很慢的情況
一條 SQL 大多數(shù)情況正常,偶爾才能出現(xiàn)很慢的情況涩禀,針對(duì)這種情況料滥,我覺得這條SQL語句的書寫本身是沒什么問題的,而是其他原因?qū)е碌陌菚?huì)是什么原因呢葵腹?
1高每、數(shù)據(jù)庫在刷新臟頁我也無奈啊
當(dāng)我們要往數(shù)據(jù)庫插入一條數(shù)據(jù)、或者要更新一條數(shù)據(jù)的時(shí)候践宴,我們知道數(shù)據(jù)庫會(huì)在內(nèi)存中把對(duì)應(yīng)字段的數(shù)據(jù)更新了鲸匿,但是更新之后,這些更新的字段并不會(huì)馬上同步持久化到磁盤中去阻肩,而是把這些更新的記錄寫入到 redo log 日記中去带欢,等到空閑的時(shí)候,在通過 redo log 里的日記把最新的數(shù)據(jù)同步到磁盤中去磺浙。
不過洪囤,redo log 里的容量是有限的徒坡,如果數(shù)據(jù)庫一直很忙撕氧,更新又很頻繁,這個(gè)時(shí)候 redo log 很快就會(huì)被寫滿了喇完,這個(gè)時(shí)候就沒辦法等到空閑的時(shí)候再把數(shù)據(jù)同步到磁盤的伦泥,只能暫停其他操作,全身心來把數(shù)據(jù)同步到磁盤中去的锦溪,而這個(gè)時(shí)候不脯,就會(huì)導(dǎo)致我們平時(shí)正常的SQL語句突然執(zhí)行的很慢,所以說刻诊,數(shù)據(jù)庫在在同步數(shù)據(jù)到磁盤的時(shí)候防楷,就有可能導(dǎo)致我們的SQL語句執(zhí)行的很慢了。
2则涯、拿不到鎖我能怎么辦
這個(gè)就比較容易想到了复局,我們要執(zhí)行的這條語句,剛好這條語句涉及到的表粟判,別人在用亿昏,并且加鎖了,我們拿不到鎖档礁,只能慢慢等待別人釋放鎖了角钩。或者呻澜,表沒有加鎖递礼,但要使用到的某個(gè)一行被加鎖了,這個(gè)時(shí)候羹幸,我也沒辦法啊脊髓。
如果要判斷是否真的在等待鎖,我們可以用 show processlist這個(gè)命令來查看當(dāng)前的狀態(tài)哦睹欲,這里我要提醒一下供炼,有些命令最好記錄一下一屋,反正,我被問了好幾個(gè)命令袋哼,都不知道怎么寫冀墨,呵呵。
下來我們來訪分析下第二種情況涛贯,我覺得第二種情況的分析才是最重要的
三诽嘉、針對(duì)一直都這么慢的情況
如果在數(shù)據(jù)量一樣大的情況下,這條 SQL 語句每次都執(zhí)行的這么慢弟翘,那就就要好好考慮下你的 SQL 書寫了虫腋,下面我們來分析下哪些原因會(huì)導(dǎo)致我們的 SQL 語句執(zhí)行的很不理想。
我們先來假設(shè)我們有一個(gè)表稀余,表里有下面兩個(gè)字段,分別是主鍵 id悦冀,和兩個(gè)普通字段 c 和 d。
mysql> CREATE TABLE `t` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
1睛琳、扎心了盒蟆,沒用到索引
(1)、字段沒有索引
剛好你的 c 字段上沒有索引师骗,那么抱歉历等,只能走全表掃描了,你就體驗(yàn)不會(huì)索引帶來的樂趣了辟癌,所以寒屯,這回導(dǎo)致這條查詢語句很慢。
(2)黍少、字段有索引寡夹,但卻沒有用索引
好吧,這個(gè)時(shí)候你給 c 這個(gè)字段加上了索引仍侥,然后又查詢了一條語句
select * from t where c - 1 = 1000;
我想問大家一個(gè)問題要出,這樣子在查詢的時(shí)候會(huì)用索引查詢嗎?
答是不會(huì)农渊,如果我們?cè)谧侄蔚淖筮呑隽诉\(yùn)算患蹂,那么很抱歉,在查詢的時(shí)候砸紊,就不會(huì)用上索引了传于,所以呢,大家要注意這種字段上有索引醉顽,但由于自己的疏忽沼溜,導(dǎo)致系統(tǒng)沒有使用索引的情況了。
正確的查詢應(yīng)該如下
select * from t where c = 1000 + 1;
有人可能會(huì)說游添,右邊有運(yùn)算就能用上索引系草?難道數(shù)據(jù)庫就不會(huì)自動(dòng)幫我們優(yōu)化一下通熄,自動(dòng)把 c - 1=1000 自動(dòng)轉(zhuǎn)換為 c = 1000+1。
不好意思找都,確實(shí)不會(huì)幫你唇辨,所以,你要注意了能耻。
(3)赏枚、函數(shù)操作導(dǎo)致沒有用上索引
如果我們?cè)诓樵兊臅r(shí)候,對(duì)字段進(jìn)行了函數(shù)操作晓猛,也是會(huì)導(dǎo)致沒有用上索引的饿幅,例如
select * from t where pow(c,2) = 1000;
這里我只是做一個(gè)例子,假設(shè)函數(shù) pow 是求 c 的 n 次方戒职,實(shí)際上可能并沒有 pow(c,2)這個(gè)函數(shù)栗恩。其實(shí)這個(gè)和上面在左邊做運(yùn)算也是很類似的。
所以呢帕涌,一條語句執(zhí)行都很慢的時(shí)候摄凡,可能是該語句沒有用上索引了续徽,不過具體是啥原因?qū)е聸]有用上索引的呢蚓曼,你就要會(huì)分析了,我上面列舉的三個(gè)原因钦扭,應(yīng)該是出現(xiàn)的比較多的吧纫版。
2、呵呵客情,數(shù)據(jù)庫自己選錯(cuò)索引了
我們?cè)谶M(jìn)行查詢操作的時(shí)候其弊,例如
select * from t where 100 < c and c < 100000;
我們知道,主鍵索引和非主鍵索引是有區(qū)別的膀斋,主鍵索引存放的值是整行字段的數(shù)據(jù)梭伐,而非主鍵索引上存放的值不是整行字段的數(shù)據(jù),而且存放主鍵字段的值仰担。不大懂的可以看我這篇文章:面試小知識(shí):MySQL索引相關(guān) 里面有說到主鍵索引和非主鍵索引的區(qū)別
也就是說糊识,我們?nèi)绻?c 這個(gè)字段的索引的話,最后會(huì)查詢到對(duì)應(yīng)主鍵的值摔蓝,然后赂苗,再根據(jù)主鍵的值走主鍵索引,查詢到整行數(shù)據(jù)返回贮尉。
好吧扯了這么多拌滋,其實(shí)我就是想告訴你,就算你在 c 字段上有索引猜谚,系統(tǒng)也并不一定會(huì)走 c 這個(gè)字段上的索引败砂,而是有可能會(huì)直接掃描掃描全表赌渣,找出所有符合 100 < c and c < 100000 的數(shù)據(jù)。
為什么會(huì)這樣呢昌犹?
其實(shí)是這樣的锡垄,系統(tǒng)在執(zhí)行這條語句的時(shí)候,會(huì)進(jìn)行預(yù)測:究竟是走 c 索引掃描的行數(shù)少祭隔,還是直接掃描全表掃描的行數(shù)少呢货岭?顯然,掃描行數(shù)越少當(dāng)然越好了疾渴,因?yàn)閽呙栊袛?shù)越少千贯,意味著I/O操作的次數(shù)越少。
如果是掃描全表的話搞坝,那么掃描的次數(shù)就是這個(gè)表的總行數(shù)了搔谴,假設(shè)為 n;而如果走索引 c 的話桩撮,我們通過索引 c 找到主鍵之后敦第,還得再通過主鍵索引來找我們整行的數(shù)據(jù),也就是說店量,需要走兩次索引芜果。而且,我們也不知道符合 100 c < and c < 10000 這個(gè)條件的數(shù)據(jù)有多少行融师,萬一這個(gè)表是全部數(shù)據(jù)都符合呢右钾?這個(gè)時(shí)候意味著,走 c 索引不僅掃描的行數(shù)是 n旱爆,同時(shí)還得每行數(shù)據(jù)走兩次索引舀射。
所以呢,系統(tǒng)是有可能走全表掃描而不走索引的怀伦。那系統(tǒng)是怎么判斷呢脆烟?
判斷來源于系統(tǒng)的預(yù)測,也就是說房待,如果要走 c 字段索引的話邢羔,系統(tǒng)會(huì)預(yù)測走 c 字段索引大概需要掃描多少行。如果預(yù)測到要掃描的行數(shù)很多吴攒,它可能就不走索引而直接掃描全表了张抄。
那么問題來了,系統(tǒng)是怎么預(yù)測判斷的呢洼怔?這里我給你講下系統(tǒng)是怎么判斷的吧署惯,雖然這個(gè)時(shí)候我已經(jīng)寫到脖子有點(diǎn)酸了。
系統(tǒng)是通過索引的區(qū)分度來判斷的镣隶,一個(gè)索引上不同的值越多极谊,意味著出現(xiàn)相同數(shù)值的索引越少诡右,意味著索引的區(qū)分度越高。我們也把區(qū)分度稱之為基數(shù)轻猖,即區(qū)分度越高帆吻,基數(shù)越大。所以呢咙边,基數(shù)越大猜煮,意味著符合 100 < c and c < 10000 這個(gè)條件的行數(shù)越少。
所以呢败许,一個(gè)索引的基數(shù)越大王带,意味著走索引查詢?cè)接袃?yōu)勢。
那么問題來了市殷,怎么知道這個(gè)索引的基數(shù)呢愕撰?
系統(tǒng)當(dāng)然是不會(huì)遍歷全部來獲得一個(gè)索引的基數(shù)的,代價(jià)太大了醋寝,索引系統(tǒng)是通過遍歷部分?jǐn)?shù)據(jù)搞挣,也就是通過采樣的方式,來預(yù)測索引的基數(shù)的音羞。
扯了這么多囱桨,重點(diǎn)的來了低斋,居然是采樣妙蔗,那就有可能出現(xiàn)失誤的情況,也就是說,c 這個(gè)索引的基數(shù)實(shí)際上是很大的办陷,但是采樣的時(shí)候,卻很不幸律歼,把這個(gè)索引的基數(shù)預(yù)測成很小民镜。例如你采樣的那一部分?jǐn)?shù)據(jù)剛好基數(shù)很小,然后就誤以為索引的基數(shù)很小险毁。然后就呵呵制圈,系統(tǒng)就不走 c 索引了,直接走全部掃描了畔况。
所以呢鲸鹦,說了這么多,得出結(jié)論:由于統(tǒng)計(jì)的失誤跷跪,導(dǎo)致系統(tǒng)沒有走索引馋嗜,而是走了全表掃描,而這吵瞻,也是導(dǎo)致我們 SQL 語句執(zhí)行的很慢的原因葛菇。
這里我聲明一下甘磨,系統(tǒng)判斷是否走索引,掃描行數(shù)的預(yù)測其實(shí)只是原因之一眯停,這條查詢語句是否需要使用使用臨時(shí)表济舆、是否需要排序等也是會(huì)影響系統(tǒng)的選擇的。
不過呢莺债,我們有時(shí)候也可以通過強(qiáng)制走索引的方式來查詢滋觉,例如
select * from t force index(a) where c < 100 and c < 100000;
我們也可以通過
show index from t;
來查詢索引的基數(shù)和實(shí)際是否符合,如果和實(shí)際很不符合的話齐邦,我們可以重新來統(tǒng)計(jì)索引的基數(shù)椎瘟,可以用這條命令
analyze table t;
來重新統(tǒng)計(jì)分析。
既然會(huì)預(yù)測錯(cuò)索引的基數(shù)侄旬,這也意味著肺蔚,當(dāng)我們的查詢語句有多個(gè)索引的時(shí)候,系統(tǒng)有可能也會(huì)選錯(cuò)索引哦儡羔,這也可能是 SQL 執(zhí)行的很慢的一個(gè)原因宣羊。
好吧,就先扯這么多了汰蜘,你到時(shí)候能扯出這么多仇冯,我覺得已經(jīng)很棒了,下面做一個(gè)總結(jié)族操。
四苛坚、總結(jié)
以上是我的總結(jié)與理解,最后一個(gè)部分色难,我怕很多人不大懂數(shù)據(jù)庫居然會(huì)選錯(cuò)索引泼舱,所以我詳細(xì)解釋了一下,下面我對(duì)以上做一個(gè)總結(jié)枷莉。
一個(gè) SQL 執(zhí)行的很慢娇昙,我們要分兩種情況討論:
1、大多數(shù)情況下很正常笤妙,偶爾很慢冒掌,則有如下原因
(1)、數(shù)據(jù)庫在刷新臟頁蹲盘,例如 redo log 寫滿了需要同步到磁盤股毫。
(2)、執(zhí)行的時(shí)候召衔,遇到鎖铃诬,如表鎖、行鎖。
2氧急、這條 SQL 語句一直執(zhí)行的很慢颗胡,則有如下原因。
(1)吩坝、沒有用上索引:例如該字段沒有索引毒姨;由于對(duì)字段進(jìn)行運(yùn)算、函數(shù)操作導(dǎo)致無法用索引钉寝。
(2)弧呐、數(shù)據(jù)庫選錯(cuò)了索引。