一条SQL语句执行得很慢的原因有哪些?
一、開(kāi)始裝逼:分類(lèi)討論
一條?SQL?語(yǔ)句執(zhí)行的很慢,那是每次執(zhí)行都很慢呢?還是大多數(shù)情況下是正常的,偶爾出現(xiàn)很慢呢?所以我覺(jué)得,我們還得分以下兩種情況來(lái)討論。
1、大多數(shù)情況是正常的,只是偶爾會(huì)出現(xiàn)很慢的情況。
2、在數(shù)據(jù)量不變的情況下,這條SQL語(yǔ)句一直以來(lái)都執(zhí)行的很慢。
針對(duì)這兩種情況,我們來(lái)分析下可能是哪些原因?qū)е碌摹?/p>
二、針對(duì)偶爾很慢的情況
一條?SQL?大多數(shù)情況正常,偶爾才能出現(xiàn)很慢的情況,針對(duì)這種情況,我覺(jué)得這條SQL語(yǔ)句的書(shū)寫(xiě)本身是沒(méi)什么問(wèn)題的,而是其他原因?qū)е碌?#xff0c;那會(huì)是什么原因呢?
1、數(shù)據(jù)庫(kù)在刷新臟頁(yè)(flush)我也無(wú)奈啊
當(dāng)我們要往數(shù)據(jù)庫(kù)插入一條數(shù)據(jù)、或者要更新一條數(shù)據(jù)的時(shí)候,我們知道數(shù)據(jù)庫(kù)會(huì)在內(nèi)存中把對(duì)應(yīng)字段的數(shù)據(jù)更新了,但是更新之后,這些更新的字段并不會(huì)馬上同步持久化到磁盤(pán)中去,而是把這些更新的記錄寫(xiě)入到 redo log 日記中去,等到空閑的時(shí)候,在通過(guò) redo log 里的日記把最新的數(shù)據(jù)同步到磁盤(pán)中去。
當(dāng)內(nèi)存數(shù)據(jù)頁(yè)跟磁盤(pán)數(shù)據(jù)頁(yè)內(nèi)容不一致的時(shí)候,我們稱(chēng)這個(gè)內(nèi)存頁(yè)為“臟頁(yè)”。內(nèi)存數(shù)據(jù)寫(xiě)入到磁盤(pán)后,內(nèi)存和磁盤(pán)上的數(shù)據(jù)頁(yè)的內(nèi)容就一致了,稱(chēng)為“干凈頁(yè)”。
刷臟頁(yè)有下面4種場(chǎng)景(后兩種不用太關(guān)注“性能”問(wèn)題):
-
redolog寫(xiě)滿(mǎn)了:redo log 里的容量是有限的,如果數(shù)據(jù)庫(kù)一直很忙,更新又很頻繁,這個(gè)時(shí)候 redo log 很快就會(huì)被寫(xiě)滿(mǎn)了,這個(gè)時(shí)候就沒(méi)辦法等到空閑的時(shí)候再把數(shù)據(jù)同步到磁盤(pán)的,只能暫停其他操作,全身心來(lái)把數(shù)據(jù)同步到磁盤(pán)中去的,而這個(gè)時(shí)候,就會(huì)導(dǎo)致我們平時(shí)正常的SQL語(yǔ)句突然執(zhí)行的很慢,所以說(shuō),數(shù)據(jù)庫(kù)在在同步數(shù)據(jù)到磁盤(pán)的時(shí)候,就有可能導(dǎo)致我們的SQL語(yǔ)句執(zhí)行的很慢了。
-
內(nèi)存不夠用了:如果一次查詢(xún)較多的數(shù)據(jù),恰好碰到所查數(shù)據(jù)頁(yè)不在內(nèi)存中時(shí),需要申請(qǐng)內(nèi)存,而此時(shí)恰好內(nèi)存不足的時(shí)候就需要淘汰一部分內(nèi)存數(shù)據(jù)頁(yè),如果是干凈頁(yè),就直接釋放,如果恰好是臟頁(yè)就需要刷臟頁(yè)。
-
MySQL 認(rèn)為系統(tǒng)“空閑”的時(shí)候:這時(shí)系統(tǒng)沒(méi)什么壓力。
-
MySQL 正常關(guān)閉的時(shí)候:這時(shí)候,MySQL 會(huì)把內(nèi)存的臟頁(yè)都 flush 到磁盤(pán)上,這樣下次 MySQL 啟動(dòng)的時(shí)候,就可以直接從磁盤(pán)上讀數(shù)據(jù),啟動(dòng)速度會(huì)很快。
2、拿不到鎖我能怎么辦
這個(gè)就比較容易想到了,我們要執(zhí)行的這條語(yǔ)句,剛好這條語(yǔ)句涉及到的表,別人在用,并且加鎖了,我們拿不到鎖,只能慢慢等待別人釋放鎖了?;蛘?#xff0c;表沒(méi)有加鎖,但要使用到的某個(gè)一行被加鎖了,這個(gè)時(shí)候,我也沒(méi)辦法啊。
如果要判斷是否真的在等待鎖,我們可以用?show processlist這個(gè)命令來(lái)查看當(dāng)前的狀態(tài)哦,這里我要提醒一下,有些命令最好記錄一下,反正,我被問(wèn)了好幾個(gè)命令,都不知道怎么寫(xiě),呵呵。
下來(lái)我們來(lái)訪分析下第二種情況,我覺(jué)得第二種情況的分析才是最重要的
3、復(fù)現(xiàn)步驟
直接看下面這種情況
此時(shí)的事務(wù)的隔離級(jí)別為可重復(fù)讀。session B 更新完 100 萬(wàn)次,生成了 100 萬(wàn)個(gè)回滾日志 (undo log)。 帶 lock in share mode 的 SQL 語(yǔ)句,是當(dāng)前讀,因此會(huì)直接讀到 1000001 這個(gè)結(jié)果,所以速 度很快;而 select * from t where id=1 這個(gè)語(yǔ)句,是一致性讀,因此需要從 1000001 開(kāi)始, 依次執(zhí)行 undo log,執(zhí)行了 100 萬(wàn)次以后,才將 1 這個(gè)結(jié)果返回。
三、針對(duì)一直都這么慢的情況
如果在數(shù)據(jù)量一樣大的情況下,這條?SQL?語(yǔ)句每次都執(zhí)行的這么慢,那就就要好好考慮下你的?SQL?書(shū)寫(xiě)了,下面我們來(lái)分析下哪些原因會(huì)導(dǎo)致我們的?SQL?語(yǔ)句執(zhí)行的很不理想。
我們先來(lái)假設(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、扎心了,沒(méi)用到索引
沒(méi)有用上索引,我覺(jué)得這個(gè)原因是很多人都能想到的,例如你要查詢(xún)這條語(yǔ)句
select?*?from?t?where?100?<c?and?c?<?100000;(1)、字段沒(méi)有索引
剛好你的?c?字段上沒(méi)有索引,那么抱歉,只能走全表掃描了,你就體驗(yàn)不會(huì)索引帶來(lái)的樂(lè)趣了,所以,這回導(dǎo)致這條查詢(xún)語(yǔ)句很慢。
(2)、字段有索引,但卻沒(méi)有用索引
好吧,這個(gè)時(shí)候你給 c 這個(gè)字段加上了索引,然后又查詢(xún)了一條語(yǔ)句
select?*?from?t?where?c?-?1?=?1000;我想問(wèn)大家一個(gè)問(wèn)題,這樣子在查詢(xún)的時(shí)候會(huì)用索引查詢(xún)嗎?
答是不會(huì),如果我們?cè)谧侄蔚淖筮呑隽诉\(yùn)算,那么很抱歉,在查詢(xún)的時(shí)候,就不會(huì)用上索引了,所以呢,大家要注意這種字段上有索引,但由于自己的疏忽,導(dǎo)致系統(tǒng)沒(méi)有使用索引的情況了。
正確的查詢(xún)應(yīng)該如下
select?*?from?t?where?c?=?1000?+?1;有人可能會(huì)說(shuō),右邊有運(yùn)算就能用上索引?難道數(shù)據(jù)庫(kù)就不會(huì)自動(dòng)幫我們優(yōu)化一下,自動(dòng)把?c?-?1=1000?自動(dòng)轉(zhuǎn)換為?c?=?1000+1。
不好意思,確實(shí)不會(huì)幫你,所以,你要注意了。
(3)、函數(shù)操作導(dǎo)致沒(méi)有用上索引
如果我們?cè)诓樵?xún)的時(shí)候,對(duì)字段進(jìn)行了函數(shù)操作,也是會(huì)導(dǎo)致沒(méi)有用上索引的,例如
select?*?from?t?where?pow(c,2)?=?1000;這里我只是做一個(gè)例子,假設(shè)函數(shù)?pow?是求?c?的?n?次方,實(shí)際上可能并沒(méi)有?pow(c,2)這個(gè)函數(shù)。其實(shí)這個(gè)和上面在左邊做運(yùn)算也是很類(lèi)似的。
所以呢,一條語(yǔ)句執(zhí)行都很慢的時(shí)候,可能是該語(yǔ)句沒(méi)有用上索引了,不過(guò)具體是啥原因?qū)е聸](méi)有用上索引的呢,你就要會(huì)分析了,我上面列舉的三個(gè)原因,應(yīng)該是出現(xiàn)的比較多的吧。
2、呵呵,數(shù)據(jù)庫(kù)自己選錯(cuò)索引了
我們?cè)谶M(jìn)行查詢(xún)操作的時(shí)候,例如
select?*?from?t?where?100?<?c?and?c?<?100000;我們知道,主鍵索引和非主鍵索引是有區(qū)別的,主鍵索引存放的值是整行字段的數(shù)據(jù),而非主鍵索引上存放的值不是整行字段的數(shù)據(jù),而且存放主鍵字段的值。不大懂的可以看這篇文章:??【思維導(dǎo)圖-索引篇】搞定數(shù)據(jù)庫(kù)索引就是這么簡(jiǎn)單? 里面有說(shuō)到主鍵索引和非主鍵索引的區(qū)別
也就是說(shuō),我們?nèi)绻?c?這個(gè)字段的索引的話,最后會(huì)查詢(xún)到對(duì)應(yīng)主鍵的值,然后,再根據(jù)主鍵的值走主鍵索引,查詢(xún)到整行數(shù)據(jù)返回。
好吧扯了這么多,其實(shí)我就是想告訴你,就算你在?c?字段上有索引,系統(tǒng)也并不一定會(huì)走?c?這個(gè)字段上的索引,而是有可能會(huì)直接掃描掃描全表,找出所有符合?100?<?c?and?c?<?100000?的數(shù)據(jù)。
為什么會(huì)這樣呢?
其實(shí)是這樣的,系統(tǒng)在執(zhí)行這條語(yǔ)句的時(shí)候,會(huì)進(jìn)行預(yù)測(cè):究竟是走?c?索引掃描的行數(shù)少,還是直接掃描全表掃描的行數(shù)少呢?顯然,掃描行數(shù)越少當(dāng)然越好了,因?yàn)閽呙栊袛?shù)越少,意味著I/O操作的次數(shù)越少。
如果是掃描全表的話,那么掃描的次數(shù)就是這個(gè)表的總行數(shù)了,假設(shè)為?n;而如果走索引?c?的話,我們通過(guò)索引?c?找到主鍵之后,還得再通過(guò)主鍵索引來(lái)找我們整行的數(shù)據(jù),也就是說(shuō),需要走兩次索引。而且,我們也不知道符合?100?c?<?and?c?<?10000?這個(gè)條件的數(shù)據(jù)有多少行,萬(wàn)一這個(gè)表是全部數(shù)據(jù)都符合呢?這個(gè)時(shí)候意味著,走?c?索引不僅掃描的行數(shù)是?n,同時(shí)還得每行數(shù)據(jù)走兩次索引。
所以呢,系統(tǒng)是有可能走全表掃描而不走索引的。那系統(tǒng)是怎么判斷呢?
判斷來(lái)源于系統(tǒng)的預(yù)測(cè),也就是說(shuō),如果要走?c?字段索引的話,系統(tǒng)會(huì)預(yù)測(cè)走?c?字段索引大概需要掃描多少行。如果預(yù)測(cè)到要掃描的行數(shù)很多,它可能就不走索引而直接掃描全表了。
那么問(wèn)題來(lái)了,系統(tǒng)是怎么預(yù)測(cè)判斷的呢?這里我給你講下系統(tǒng)是怎么判斷的吧,雖然這個(gè)時(shí)候我已經(jīng)寫(xiě)到脖子有點(diǎn)酸了。
系統(tǒng)是通過(guò)索引的區(qū)分度來(lái)判斷的,一個(gè)索引上不同的值越多,意味著出現(xiàn)相同數(shù)值的索引越少,意味著索引的區(qū)分度越高。我們也把區(qū)分度稱(chēng)之為基數(shù),即區(qū)分度越高,基數(shù)越大。所以呢,基數(shù)越大,意味著符合?100?<?c?and?c?<?10000?這個(gè)條件的行數(shù)越少。
所以呢,一個(gè)索引的基數(shù)越大,意味著走索引查詢(xún)?cè)接袃?yōu)勢(shì)。
那么問(wèn)題來(lái)了,怎么知道這個(gè)索引的基數(shù)呢?
系統(tǒng)當(dāng)然是不會(huì)遍歷全部來(lái)獲得一個(gè)索引的基數(shù)的,代價(jià)太大了,索引系統(tǒng)是通過(guò)遍歷部分?jǐn)?shù)據(jù),也就是通過(guò)采樣的方式,來(lái)預(yù)測(cè)索引的基數(shù)的。
扯了這么多,重點(diǎn)的來(lái)了,居然是采樣,那就有可能出現(xiàn)失誤的情況,也就是說(shuō),c?這個(gè)索引的基數(shù)實(shí)際上是很大的,但是采樣的時(shí)候,卻很不幸,把這個(gè)索引的基數(shù)預(yù)測(cè)成很小。例如你采樣的那一部分?jǐn)?shù)據(jù)剛好基數(shù)很小,然后就誤以為索引的基數(shù)很小。然后就呵呵,系統(tǒng)就不走 c 索引了,直接走全部掃描了。
所以呢,說(shuō)了這么多,得出結(jié)論:由于統(tǒng)計(jì)的失誤,導(dǎo)致系統(tǒng)沒(méi)有走索引,而是走了全表掃描,而這,也是導(dǎo)致我們?SQL?語(yǔ)句執(zhí)行的很慢的原因。
這里我聲明一下,系統(tǒng)判斷是否走索引,掃描行數(shù)的預(yù)測(cè)其實(shí)只是原因之一,這條查詢(xún)語(yǔ)句是否需要使用使用臨時(shí)表、是否需要排序等也是會(huì)影響系統(tǒng)的選擇的。
不過(guò)呢,我們有時(shí)候也可以通過(guò)強(qiáng)制走索引的方式來(lái)查詢(xún),例如
select?*?from?t?force?index(a)?where?c?<?100?and?c?<?100000;我們也可以通過(guò)
show?index?from?t;來(lái)查詢(xún)索引的基數(shù)和實(shí)際是否符合,如果和實(shí)際很不符合的話,我們可以重新來(lái)統(tǒng)計(jì)索引的基數(shù),可以用這條命令
analyze?table?t;來(lái)重新統(tǒng)計(jì)分析。
既然會(huì)預(yù)測(cè)錯(cuò)索引的基數(shù),這也意味著,當(dāng)我們的查詢(xún)語(yǔ)句有多個(gè)索引的時(shí)候,系統(tǒng)有可能也會(huì)選錯(cuò)索引哦,這也可能是?SQL?執(zhí)行的很慢的一個(gè)原因。
好吧,就先扯這么多了,你到時(shí)候能扯出這么多,我覺(jué)得已經(jīng)很棒了,下面做一個(gè)總結(jié)。
四、總結(jié)
以上是我的總結(jié)與理解,最后一個(gè)部分,我怕很多人不大懂數(shù)據(jù)庫(kù)居然會(huì)選錯(cuò)索引,所以我詳細(xì)解釋了一下,下面我對(duì)以上做一個(gè)總結(jié)。
一個(gè)?SQL?執(zhí)行的很慢,我們要分兩種情況討論:
1、大多數(shù)情況下很正常,偶爾很慢,則有如下原因
(1)、數(shù)據(jù)庫(kù)在刷新臟頁(yè),例如?redo?log?寫(xiě)滿(mǎn)了需要同步到磁盤(pán)。
(2)、執(zhí)行的時(shí)候,遇到鎖,如表鎖、行鎖。
2、這條?SQL?語(yǔ)句一直執(zhí)行的很慢,則有如下原因。
(1)、沒(méi)有用上索引:例如該字段沒(méi)有索引;由于對(duì)字段進(jìn)行運(yùn)算、函數(shù)操作導(dǎo)致無(wú)法用索引。
(2)、數(shù)據(jù)庫(kù)選錯(cuò)了索引。
總結(jié)
以上是生活随笔為你收集整理的一条SQL语句执行得很慢的原因有哪些?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: AQS抽象队列同步器详解(待更新)
- 下一篇: Mysql剖析单条查询三种方法