天天看點

為什麼我這條SQL執行那麼慢?

為什麼我這條SQL執行那麼慢?

本文公衆号來源:苦逼的碼農 作者:帥地

一條SQL執行語句很慢有什麼原因?

說實話,這個問題可以涉及到 MySQL 的很多核心知識,可以扯出一大堆,就像要考你計算機網絡的知識時,問你“輸入URL回車之後,究竟發生了什麼”一樣,看看你能說出多少了。

之前騰訊面試的實話,也問到這個問題了,不過答的很不好,之前沒去想過相關原因,導緻一時之間扯不出來。是以今天,我帶大家來詳細扯一下有哪些原因,相信你看完之後一定會有所收獲,不然你打我。

一、開始裝逼:分類讨論

一條 SQL 語句執行的很慢,那是每次執行都很慢呢?還是大多數情況下是正常的,偶爾出現很慢呢?是以我覺得,我們還得分以下兩種情況來讨論。

1、大多數情況是正常的,隻是偶爾會出現很慢的情況。

2、在資料量不變的情況下,這條SQL語句一直以來都執行的很慢。

針對這兩種情況,我們來分析下可能是哪些原因導緻的。

二、針對偶爾很慢的情況

一條 SQL 大多數情況正常,偶爾才能出現很慢的情況,針對這種情況,我覺得這條SQL語句的書寫本身是沒什麼問題的,而是其他原因導緻的,那會是什麼原因呢?

1、資料庫在重新整理髒頁我也無奈啊

當我們要往資料庫插入一條資料、或者要更新一條資料的時候,我們知道資料庫會在記憶體中把對應字段的資料更新了,但是更新之後,這些更新的字段并不會馬上同步持久化到磁盤中去,而是把這些更新的記錄寫入到 redo log 日記中去,等到空閑的時候,在通過 redo log 裡的日記把最新的資料同步到磁盤中去。

不過,redo log 裡的容量是有限的,如果資料庫一直很忙,更新又很頻繁,這個時候 redo log 很快就會被寫滿了,這個時候就沒辦法等到空閑的時候再把資料同步到磁盤的,隻能暫停其他操作,全身心來把資料同步到磁盤中去的,而這個時候,就會導緻我們平時正常的SQL語句突然執行的很慢,是以說,資料庫在在同步資料到磁盤的時候,就有可能導緻我們的SQL語句執行的很慢了。

2、拿不到鎖我能怎麼辦

這個就比較容易想到了,我們要執行的這條語句,剛好這條語句涉及到的表,别人在用,并且加鎖了,我們拿不到鎖,隻能慢慢等待别人釋放鎖了。或者,表沒有加鎖,但要使用到的某個一行被加鎖了,這個時候,我也沒辦法啊。

如果要判斷是否真的在等待鎖,我們可以用 show processlist這個指令來檢視目前的狀态哦,這裡我要提醒一下,有些指令最好記錄一下,反正,我被問了好幾個指令,都不知道怎麼寫,呵呵。

下來我們來訪分析下第二種情況,我覺得第二種情況的分析才是最重要的

三、針對一直都這麼慢的情況

如果在資料量一樣大的情況下,這條 SQL 語句每次都執行的這麼慢,那就就要好好考慮下你的 SQL 書寫了,下面我們來分析下哪些原因會導緻我們的 SQL 語句執行的很不理想。

我們先來假設我們有一個表,表裡有下面兩個字段,分别是主鍵 id,和兩個普通字段 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、紮心了,沒用到索引

沒有用上索引,我覺得這個原因是很多人都能想到的,例如你要查詢這條語句

select * from t where 100 <c and c < 100000;           

複制

(1)、字段沒有索引

剛好你的 c 字段上沒有索引,那麼抱歉,隻能走全表掃描了,你就體驗不會索引帶來的樂趣了,是以,這回導緻這條查詢語句很慢。

(2)、字段有索引,但卻沒有用索引

好吧,這個時候你給 c 這個字段加上了索引,然後又查詢了一條語句

select * from t where c - 1 = 1000;           

複制

我想問大家一個問題,這樣子在查詢的時候會用索引查詢嗎?

答是不會,如果我們在字段的左邊做了運算,那麼很抱歉,在查詢的時候,就不會用上索引了,是以呢,大家要注意這種字段上有索引,但由于自己的疏忽,導緻系統沒有使用索引的情況了。

正确的查詢應該如下

select * from t where c = 1000 + 1;           

複制

有人可能會說,右邊有運算就能用上索引?難道資料庫就不會自動幫我們優化一下,自動把 c - 1=1000 自動轉換為 c = 1000+1。

不好意思,确實不會幫你,是以,你要注意了。

(3)、函數操作導緻沒有用上索引

如果我們在查詢的時候,對字段進行了函數操作,也是會導緻沒有用上索引的,例如

select * from t where pow(c,2) = 1000;           

複制

這裡我隻是做一個例子,假設函數 pow 是求 c 的 n 次方,實際上可能并沒有 pow(c,2)這個函數。其實這個和上面在左邊做運算也是很類似的。

是以呢,一條語句執行都很慢的時候,可能是該語句沒有用上索引了,不過具體是啥原因導緻沒有用上索引的呢,你就要會分析了,我上面列舉的三個原因,應該是出現的比較多的吧。

2、呵呵,資料庫自己選錯索引了

我們在進行查詢操作的時候,例如

select * from t where 100 < c and c < 100000;           

複制

我們知道,主鍵索引和非主鍵索引是有差別的,主鍵索引存放的值是整行字段的資料,而非主鍵索引上存放的值不是整行字段的資料,而且存放主鍵字段的值。不大懂的可以看我這篇文章:面試小知識:MySQL索引相關 裡面有說到主鍵索引和非主鍵索引的差別

也就是說,我們如果走 c 這個字段的索引的話,最後會查詢到對應主鍵的值,然後,再根據主鍵的值走主鍵索引,查詢到整行資料傳回。

好吧扯了這麼多,其實我就是想告訴你,就算你在 c 字段上有索引,系統也并不一定會走 c 這個字段上的索引,而是有可能會直接掃描掃描全表,找出所有符合 100 < c and c < 100000 的資料。

為什麼會這樣呢?

其實是這樣的,系統在執行這條語句的時候,會進行預測:究竟是走 c 索引掃描的行數少,還是直接掃描全表掃描的行數少呢?顯然,掃描行數越少當然越好了,因為掃描行數越少,意味着I/O操作的次數越少。

如果是掃描全表的話,那麼掃描的次數就是這個表的總行數了,假設為 n;而如果走索引 c 的話,我們通過索引 c 找到主鍵之後,還得再通過主鍵索引來找我們整行的資料,也就是說,需要走兩次索引。而且,我們也不知道符合 100 c < and c < 10000 這個條件的資料有多少行,萬一這個表是全部資料都符合呢?這個時候意味着,走 c 索引不僅掃描的行數是 n,同時還得每行資料走兩次索引。

是以呢,系統是有可能走全表掃描而不走索引的。那系統是怎麼判斷呢?

判斷來源于系統的預測,也就是說,如果要走 c 字段索引的話,系統會預測走 c 字段索引大概需要掃描多少行。如果預測到要掃描的行數很多,它可能就不走索引而直接掃描全表了。

那麼問題來了,系統是怎麼預測判斷的呢?這裡我給你講下系統是怎麼判斷的吧,雖然這個時候我已經寫到脖子有點酸了。

系統是通過索引的區分度來判斷的,一個索引上不同的值越多,意味着出現相同數值的索引越少,意味着索引的區分度越高。我們也把區分度稱之為基數,即區分度越高,基數越大。是以呢,基數越大,意味着符合 100 < c and c < 10000 這個條件的行數越少。

是以呢,一個索引的基數越大,意味着走索引查詢越有優勢。

那麼問題來了,怎麼知道這個索引的基數呢?

系統當然是不會周遊全部來獲得一個索引的基數的,代價太大了,索引系統是通過周遊部分資料,也就是通過采樣的方式,來預測索引的基數的。

扯了這麼多,重點的來了,居然是采樣,那就有可能出現失誤的情況,也就是說,c 這個索引的基數實際上是很大的,但是采樣的時候,卻很不幸,把這個索引的基數預測成很小。例如你采樣的那一部分資料剛好基數很小,然後就誤以為索引的基數很小。然後就呵呵,系統就不走 c 索引了,直接走全部掃描了。

是以呢,說了這麼多,得出結論:由于統計的失誤,導緻系統沒有走索引,而是走了全表掃描,而這,也是導緻我們 SQL 語句執行的很慢的原因。

這裡我聲明一下,系統判斷是否走索引,掃描行數的預測其實隻是原因之一,這條查詢語句是否需要使用使用臨時表、是否需要排序等也是會影響系統的選擇的。

不過呢,我們有時候也可以通過強制走索引的方式來查詢,例如

select * from t force index(a) where c < 100 and c < 100000;           

複制

我們也可以通過

show index from t;           

複制

來查詢索引的基數和實際是否符合,如果和實際很不符合的話,我們可以重新來統計索引的基數,可以用這條指令

analyze table t;           

複制

來重新統計分析。

既然會預測錯索引的基數,這也意味着,當我們的查詢語句有多個索引的時候,系統有可能也會選錯索引哦,這也可能是 SQL 執行的很慢的一個原因。

好吧,就先扯這麼多了,你到時候能扯出這麼多,我覺得已經很棒了,下面做一個總結。

四、總結

以上是我的總結與了解,最後一個部分,我怕很多人不大懂資料庫居然會選錯索引,是以我詳細解釋了一下,下面我對以上做一個總結。

一個 SQL 執行的很慢,我們要分兩種情況讨論:

1、大多數情況下很正常,偶爾很慢,則有如下原因

(1)、資料庫在重新整理髒頁,例如 redo log 寫滿了需要同步到磁盤。

(2)、執行的時候,遇到鎖,如表鎖、行鎖。

2、這條 SQL 語句一直執行的很慢,則有如下原因。

(1)、沒有用上索引:例如該字段沒有索引;由于對字段進行運算、函數操作導緻無法用索引。

(2)、資料庫選錯了索引。

大家如果有補充的,也是可以留言區補充一波哦。