1. 程式人生 > 其它 >如何根治慢SQL?

如何根治慢SQL?

本文摘自:CodeSheep

今天和大家聊一個常見的問題:慢SQL。

包括以下內容:

  • 慢SQL的危害
  • SQL語句的執行過程
  • 儲存引擎和索引的那些事兒
  • 慢SQL解決之道

後續均以MySQL預設儲存引擎InnoDB為例進行展開,話不多說,開始!

1.慢SQL的危害

慢SQL,就是跑得很慢的SQL語句,你可能會問慢SQL會有啥問題嗎?

試想一個場景:

大白和小黑端午出去玩,機票太貴於是買了高鐵,火車站的人真是烏央烏央的。

馬上檢票了,大白和小黑準備去廁所清理下庫存,坑位不多,排隊的人還真不少。

小黑髮現其中有3個坑的乘客賊慢,其他2個坑位換了好幾波人,這3位坑主就是不出來。

等在外面的大夥,心裡很是不爽,長期佔用公共資源,後面的人沒法用。

小黑苦笑道:這不就是廁所版的慢SQL嘛!

這是實際生活中的例子,換到MySQL伺服器也是一樣的,畢竟科技源自生活嘛。

MySQL伺服器的資源(CPU、IO、記憶體等)是有限的,尤其在高併發場景下需要快速處理掉請求,否則一旦出現慢SQL就會阻塞掉很多正常的請求,造成大面積的失敗/超時等。

2.SQL語句執行過程

客戶端和MySQL服務端的互動過程簡介:

  1. 客戶端傳送一條SQL語句給服務端,服務端的聯結器先進行賬號/密碼、許可權等環節驗證,有異常直接拒絕請求。
  2. 服務端查詢快取,如果SQL語句命中了快取,則返回快取中的結果,否則繼續處理。
  3. 服務端對SQL語句進行詞法解析、語法解析、預處理來檢查SQL語句的合法性。
  4. 服務端通過優化器對之前生成的解析樹進行優化處理,生成最優的物理執行計劃。
  5. 將生成的物理執行計劃呼叫儲存引擎的相關介面,進行資料查詢和處理。
  6. 處理完成後將結果返回客戶端。

客戶端和MySQL服務端的互動過程簡圖:

俗話說"條條大路通羅馬",優化器的作用就是找到這麼多路中最優的那一條。

儲存引擎更是決定SQL執行的核心元件,適當瞭解其中原理十分有益。

3. 儲存引擎和索引的那些事兒

3.1 儲存引擎

InnoDB儲存引擎(Storage Engine)是MySQL預設之選,所以非常典型。

儲存引擎的主要作用是進行資料的存取和檢索,也是真正執行SQL語句的元件。

InnoDB的整體架構分為兩個部分:記憶體架構和磁碟架構,如圖:

儲存引擎的內容非常多,並不是一篇文章能說清楚的,本文不過多展開,我們在此只需要瞭解記憶體架構和磁碟架構的大致組成即可。

InnoDB 引擎是面向行儲存的,資料都是儲存在磁碟的資料頁中,資料頁裡面按照固定的行格式儲存著每一行資料。

行格式主要分為四種類型Compact、Redundant、Dynamic和Compressed,預設為Compact格式。

磁碟預讀機制和區域性性原理

當計算機訪問一個數據時,不僅會載入當前資料所在的資料頁,還會將當前資料頁相鄰的資料頁一同載入到記憶體,磁碟預讀的長度一般為頁的整倍數,從而有效降低磁碟IO的次數。

磁碟和記憶體的互動

MySQL中磁碟的資料需要被交換到記憶體,才能完成一次SQL互動,大致如圖:

  • 扇區是硬碟的讀寫的基本單位,通常情況下每個扇區的大小是 512B
  • 磁碟塊檔案系統讀寫資料的最小單位,相鄰的扇區組合在一起形成一個塊,一般是4KB
  • 頁是記憶體的最小儲存單位,頁的大小通常為磁碟塊大小的 2^n 倍
  • InnoDB頁面的預設大小是16KB,是數倍個作業系統的頁

隨機磁碟IO

MySQL的資料是一行行儲存在磁碟上的,並且這些資料並非物理連續地儲存,這樣的話要查詢資料就無法避免隨機在磁碟上讀取和寫入資料。

對於MySQL來說,當出現大量磁碟隨機IO時,大部分時間都被浪費到尋道上,磁碟呼嚕呼嚕轉,就是傳輸不了多少資料。

一次磁碟訪問由三個動作組成:

  • 尋道:磁頭移動定位到指定磁軌
  • 旋轉:等待指定扇區從磁頭下旋轉經過
  • 資料傳輸:資料在磁碟與記憶體之間的實際傳輸

對於儲存引擎來說,如何有效降低隨機IO是個非常重要的問題。

3.2 索引

可以實現增刪改查的資料結構非常多,包括:雜湊表、二叉搜尋樹、AVL、紅黑樹、B樹、B+樹等,這些都是可以作為索引的候選資料結構。

結合MySQL的實際情況:磁碟和記憶體互動、隨機磁碟IO、排序和範圍查詢、增刪改的複雜度等等,綜合考量之下B+樹脫穎而出。

B+樹作為多叉平衡樹,對於範圍查詢和排序都可以很好地支援,並且更加矮胖,訪問資料時的平均磁碟IO次數取決於樹的高度,因此B+樹可以讓磁碟的查詢次數更少。

在InnoDB中B+樹的高度一般都在2~4層,並且根節點常駐記憶體中,也就是說查詢某值的行記錄時最多隻需要1~3次磁碟I/O操作。

MyISAM是將資料和索引分開儲存的,InnoDB儲存引擎的資料和索引沒有分開儲存,這也就是為什麼有人說Innodb索引即資料,資料即索引,如圖:

說到InnoDB的資料和索引的儲存,就提到一個名詞:聚集索引

聚集索引

聚集索引將索引和資料完美地融合在一起,是每個Innodb表都會有的一個特殊索引,一般來說是藉助於表的主鍵來構建的B+樹。

假設我們有student表,將id作為主鍵索引,那麼聚集索引的B+樹結構,如圖:

  • 非葉子節點不存資料,只有主鍵和相關指標
  • 葉子節點包含主鍵、行資料、指標
  • 葉子節點之間由雙向指標串聯形成有序雙向連結串列,葉子節點內部也是有序的

聚集索引按照如下規則建立:

  • 有主鍵時InnoDB利用主鍵來生成
  • 沒有主鍵,InnoDB會選擇一個非空的唯一索引來建立
  • 無主鍵且非NULL唯一索引時,InnoDB會隱式建立一個自增的列來建立

假如我們要查詢id=10的資料,大致過程如下:

  • 索引的根結點在記憶體中,10>9 因此找到P3指標
  • P3指向的資料並沒有在記憶體中,因此產生1次磁碟IO讀取磁碟塊3到記憶體
  • 在記憶體中對磁碟塊3進行二分查詢,找到ID=9的全部值

非聚集索引

非聚集索引的葉子節點中存放的是二級索引值和主鍵鍵值,非葉子節點和葉子節點都沒有儲存整行資料值。

假設我們有student表,將name作為二級索引,那麼非聚集索引的B+樹結構,如圖:

由於非聚集索引的葉子節點沒有儲存行資料,如果通過非聚集索引來查詢非二級索引值,需要分為兩步:

  • 第一:通過非聚集索引的葉子節點來確定資料行對應的主鍵
  • 第二:通過相應的主鍵值在聚集索引中查詢到對應的行記錄

我們把通過非聚集索引找到主鍵值,再根據主鍵值從聚集索引找對於行資料的過程稱為:回表查詢

換句話說:select * from student where name = 'Bob' 將產生回表查詢,因為在name索引的葉子節點沒有其他值,只能從聚集索引獲得。

所以如果查詢的欄位在非聚集索引就可以完成,就可以避免一次回表過程,這種稱為:覆蓋索引,所以select * 並不是好習慣,需要什麼拿什麼就好。

假如我們要查詢name=Tom的記錄的所有值,大致過程如下:

  • 從非聚集索引開始,根節點在記憶體中,按照name的字典序找到P3指標
  • P3指標所指向的磁碟塊不在記憶體中,產生1次磁碟IO載入到記憶體
  • 在記憶體中對磁碟塊3的資料進行搜尋,獲得name=tom的記錄的主鍵值為4
  • 根據主鍵值4從聚集索引的根節點中獲得P2指標
  • P2指標所指向的磁碟塊不在記憶體中,產生第2次磁碟IO載入到記憶體
  • 將上一步獲得的資料,在記憶體中進行二分查詢獲得全部行資料

上述查詢就包含了一次回表過程,因此效能比主鍵查詢慢了一倍,因此儘量使用主鍵查詢,一次完事。

4. 慢SQL解決思路

出現慢SQL的原因很多,我們拋開單表數億記錄和無索引的特殊情況,來討論一些更有普遍意義的慢SQL原因和解決之道。

我們從兩個方面來進行闡述:

  • 資料庫表索引設定不合理
  • SQL語句有問題,需要優化

4.1 索引設定原則

程式設計師的角度和儲存引擎的角度是不一樣的,索引寫的好,SQL跑得快。

  • 索引區分度低

假如表中有1000w記錄,其中有status欄位表示狀態,可能90%的資料status=1,可以不將status作為索引,因為其對資料記錄區分度很低。

  • 切忌過多建立索引

每個索引都需要佔用磁碟空間,修改表資料時會對索引進行更新,索引越多,更新越複雜。

因為每新增一個索引,.ibd檔案中就需要多維護一個B+Tree索引樹,如果某一個table中存在10個索引,那麼就需要維護10棵B+Tree,寫入效率會降低,並且會浪費磁碟空間。

  • 常用查詢欄位建索引

如果某個欄位經常用來做查詢條件,那麼該欄位的查詢速度會影響整個表的查詢速度,屬於熱門欄位,為其建立索引非常必要。

  • 常排序/分組/去重欄位建索引

對於需要經常使用ORDER BY、GROUP BY、DISTINCT和UNION等操作的欄位建立索引,可以有效藉助B+樹的特性來加速執行。

  • 主鍵和外來鍵建索引

主鍵可以用來建立聚集索引,外來鍵也是唯一的且常用於表關聯的欄位,也需要建索引來提高效能。

4.2 SQL的優化

如果資料庫表的索引設定比較合理,SQL語句書寫不當會造成索引失效,甚至造成全表掃描,迅速拉低效能。

索引失效

我們在寫SQL的時候在某些情況下會出現索引失效的情況:

  • 對索引使用函式

select id from std upper(name) = 'JIM';

  • 對索引進行運算

select id from std where id+1=10;

  • 對索引使用<> 、not in 、not exist、!=

select id from std where name != 'jim';

  • 對索引進行前導模糊查詢

select id from std name like '%jim';

  • 隱式轉換會導致不走索引

比如:字串型別索引欄位不加引號,select id from std name = 100;保持變數型別與欄位型別一致

  • 非索引欄位的or連線

並不是所有的or都會使索引失效,如果or連線的所有欄位都設定了索引,是會走索引的,一旦有一個欄位沒有索引,就會走全表掃描。

  • 聯合索引僅包含複合索引非前置列

聯合索引包含key1,key2,key3三列,但SQL語句沒有key1,根據聯合索引的最左匹配原則,不會走聯合索引。
select name from table where key2=1 and key3=2;

好的建議

  • 使用連線代替子查詢

對於資料庫來說,在絕大部分情況下,連線會比子查詢更快,使用連線的方式,MySQL優化器一般可以生成更佳的執行計劃,更高效地處理查詢
而子查詢往往需要執行重複的查詢,子查詢生成的臨時表上也沒有索引, 因此效率會更低。

  • LIMIT偏移量過大的優化

禁止分頁查詢偏移量過大,如limit 100000,10

  • 使用覆蓋索引
    減少select * 藉助覆蓋索引,減少回表查詢次數。

  • 多表關聯查詢時,小表在前,大表在後

在MySQL中,執行from後的表關聯查詢是從左往右執行的,第一張表會涉及到全表掃描,所以將小表放在前面,先掃小表,掃描快效率較高,在掃描後面的大表,或許只掃描大表的前100行就符合返回條件並return了。

  • 調整Where字句中的連線順序

MySQL採用從左往右的順序解析where子句,可以將過濾資料多的條件放在前面,最快速度縮小結果集。

  • 使用小範圍事務,而非大範圍事務

  • 遵循最左匹配原則

  • 使用聯合索引,而非建立多個單獨索引

4.3 慢SQL的分析

在分析慢SQL之前需要通過MySQL進行相關設定:

  • 開啟慢SQL日誌
  • 設定慢SQL的執行時間閾值
開啟:SET GLOBAL slow_query_log = 1;
開啟狀態:SHOW VARIABLES LIKE '%slow_query_log%';
設定閾值:SET GLOBAL long_query_time=3;
檢視閾值:SHOW GLOBAL VARIABLES LIKE 'long_query_time%';

explain分析SQL

explain命令只需要加在select之前即可,例如:

explain select * from std where id < 100;

該命令會展示sql語句的詳細執行過程,幫助我們定位問題,網上關於explain的用法和講解很多,本文不再展開。

5. 小結

本文從慢SQL的危害、Innodb儲存引擎、聚集索引、非聚集索引、索引失效、SQL優化、慢SQL分析等角度進行了闡述。如果本文能在某些方面對讀者有所啟發,足矣。