【MySQL數據庫】一條SQL語句爲什麼執行這麼慢?

面試高頻題目,一條SQL語句爲什麼執行這麼慢? 這其中涉及的知識也是各種各樣,今天就讓我們來完全剖析這個問題。

一、執行偶爾變慢

有的時候,明明執行的是同一條語句,之前執行還挺快的,但忽然某一次就像“卡住了一樣”需要很久才能返回結果,甚至是長時間不返回。出現這種情況呢,就需要考慮以下兩種情況:

1. 刷髒頁,寫磁盤

首先說明髒頁的概念:當內存數據頁跟磁盤數據頁內容不一致的時候,我們稱這個內存頁爲“髒頁”。內存數據寫入到磁盤後,內存和磁盤上的數據頁的內容就一致了,稱爲“乾淨頁”。
通常情況下,我們對於數據的操作都是將數據從磁盤中加載到內存中後,在內存中進行修改的。而修改後也並不是直接就將內存中的髒頁刷回到磁盤中。什麼情況下會進行刷髒頁的操作呢?

  1. redo log寫滿了,要flush髒頁,前面我們講過,MySQL操作時會寫redo log,而且redo log 的大小時有限的,那麼就會出現寫滿了的情況,此時,就不得不將redo log中的數據刷回到磁盤中。這種情況是InnoDB要儘量避免的。因爲出現這種情況的時候,整個系統就不能再接受更新了,所有的更新都必須堵住。
  2. 內存不夠用了,要先將髒頁寫到磁盤,這種情況其實是常態。InnoDB用緩衝池(buffer pool)管理內存,緩衝池中的內存頁有三種狀態:
    第一種是,還沒有使用的;
    第二種是,使用了並且是乾淨頁;
    第三種是,使用了並且是髒頁。
    而當要讀入的數據頁沒有在內存的時候,就必須到緩衝池中申請一個數據頁。如果內存不夠,就要把最久不使用的數據頁從內存中淘汰掉,如果是剛好是髒頁呢,就必須將髒頁先刷到磁盤,變成乾淨頁後才能複用。
  3. 系統處於空閒時刻,在系統處於空閒時刻時,就會將刷髒頁操作安排起來。
  4. MySQL正常關閉時刻,在關閉時刻,系統會將內存中的髒頁都刷回磁盤,保證下次使用可以直接使用數據。

對於第三四種情況,明顯不是在執行SQL語句導致慢的原因,重點影響效率的就是第一種和第二種情況。
📌所以如果在執行SQL語句時,發生了刷髒頁操作,那就一定會影響整體效率!

如果對redo log 還不是很理解的,推薦另一篇文章——>認識MySQL中重要的 bin log、redo log 日誌系統

2. 還在等鎖

這個相信是比較好理解的原因吧:當我們在執行一條SQL語句,需要獲得行鎖又或者是表鎖時,剛好這個表或者行上的鎖已經被別的線程持有,此時,我們要執行的語句只能等待別的事務提交後釋放鎖才能繼續執行。
在排查問題時,可以使用show processlist命令,來查看當前的狀態,然後進行響應的調整。

📌所以如過SQL語句操作需要等待獲取鎖,就會導致執行變慢 !

3. 回滾日誌過多

還有可能出現的問題就是有很多的undo log回滾日誌,由於MySQL默認的隔離級別是可重複讀,也就是一致性讀,所以就有可能出現事務A在事務B之前開啓事務並進行查詢字段a的操作,而事務B進行了大量修改字段a的操作。
所以,當事務A查到字段a的最新值時,由於隔離級別是可重複讀,它查詢出的字段a的值不應該是事務B修改後的。而事務A讀到的卻a是最新的值,但它並不會返回這個結果,而是會從a當前的值多次使用回滾日誌,最終查詢到未被事務B修改時候的值

📌由於重複進行了大量對查詢值的回滾,也會導致一條SQL語句變慢!

推薦自己的關於隔離級別的文章——>談談事務的隔離級別有什麼?

二、執行總是很慢

另一種情況呢,就是無論怎麼執行這條語句,總是很慢。

1. 沒有索引

首先想到的,就是在SQL語句中操作的字段上沒有索引,執行語句只能進行全表掃描。
如果整個表的數據很多,顯然進行全表掃描會導致整體的效率十分低。爲了解決這個問題,就要考慮在字段上加上索引,當然加索引也要選擇最適合的索引。比如是否可以根據最左前綴的原則使用聯合索引,又或者是根據是否需要回表添加主鍵索引或普通索引,還有根據業務邏輯是否需要建立唯一索引。總之要選擇最適合的索引來增加效率。

這裏也可以參考我之前的文章:
關於MySQL索引的基礎知識
普通索引和唯一索引的區別?

📌所以,如果字段上沒有索引,執行起來當然慢 !

2. 對字段進行函數操作

當你發現,查詢的字段明明是有索引的呀,怎麼執行起來還是這麼慢呀?
這時候檢查一下你的SQL語句是否對字段進行了函數操作,比如:

select * from student where id + 1 = 10000

看起來很正常,而且id這個字段上也是有索引的,按理說這個搜索是該走索引的。但是實際上,MySQL對這個語句還是進行了全表掃描,原因是它只認識id的索引,並不認識id+1,他認爲對索引字段做函數操作,可能會破壞索引值的有序性,因此優化器就決定放棄走樹搜索功能。所以這個語句由於沒有用到索引,而是進行全表掃描導致執行變慢。看起來確實有點不智能,我們只能將SQL語句修改爲:

select * from student where id = 10000 - 1

這樣寫,雖然語義沒變,但這次就可以走id 索引來查詢了。

📌所以,如果對索引字段做函數操作,執行時就不會繼續選擇這個索引,這樣也會導致效率低下 !

3. 選錯索引

看過上面的原因,你仔細覈查了一下,即有索引,而且也沒有對索引字段做函數操作呀,怎麼查詢還是慢吞吞的。這時候就考慮一下,是不是MySQL給我們選錯索引了。 你可能有點驚訝,MySQL竟然還會選錯索引嘛?! 是的,那就來一起看看這種情況吧:

首先,對於一個查詢語句而言,選擇是否使用索引和使用哪一個索引時優化器的事情,優化器判斷時會根據掃描行數是否排序等問題來進行選擇一個他認爲最合理的索引,或者是不使用索引。你可能會說,怎麼會不使用索引呢?使用索引是快的呀。

還記得之前說的回表操作嘛,如果是一個普通索引,他需要先查詢到葉子節點中存放的主鍵索引的值,再到主鍵索引樹在找出相應的數據。那麼就有可能因爲回表的問題導致優化器覺得使用索引還不如全表掃描呢。
優化器首先會預判出,使用這個索引字段會需要掃描多少行,掃描的行數越少越好。而它預估的方式就是索引的區分度,而一個索引上不同的值的個數,我們稱之爲基數(cardinality)。也就是說,這個基數越大,索引的區分度越好。

而優化器得到的基數也並不是很準確的,因爲它並不是將整張表都拿出來一個個的統計,畢竟這樣代價很大,所以它實際上時通過採樣來實現的。,InnoDB默認會選擇N個數據頁,統計這些頁面上的不同值,得到一個平均值,然後乘以這個索引的頁面數,就得到了這個索引的基數。這種方式也使得基數實際上是不是很準確的。
我們可以通過show index方法,看到一個索引的基數。也可以通過explain命令查看語句的執行情況。

所以,如果執行一個語句時,發現這個索引的區分度很小時,就會讓優化器覺得符合條件的數據會很多,也會進行很多次的回表,所以他就會放棄走索引,而使用全表掃描。

如果我們發現這個問題,怎麼解決呢?

  1. 可以使用force index命令來強行選擇一個索引。
  2. 可以修改語句,引導MySQL使用我們期望的索引。
  3. 可以新建一個更合適的索引,來提供給優化器做選擇,或刪掉誤用的索引。

📌總之,如果MySQL自己選錯了索引,也會導致整個語句執行變慢!

嘮嘮叨叨:
好啦,這就是我總結的使得一條SQL語句爲執行慢的原因了。我總結了六條,當然還會有各種各樣的場景使得整體效率變慢,本人知識廣度有限,希望還知道其它情況的大佬多多指點。本文參考極客時間課程《MySQL實戰45講》,老師講的非常好,如果感興趣建議大家去看。文章如果有什麼問題歡迎留言指正,另外如果對你有幫助也歡迎小夥伴們點贊關注一起進步!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章