今天和大家聊一個常見的問題:慢SQL。
通過本文你将了解到以下内容:
慢SQL的危害
SQL語句的執行過程
存儲引擎和索引的那些事兒
慢SQL解決之道
後續均以MySQL預設存儲引擎InnoDB為例進行展開,話不多說,開搞!
慢SQL,就是跑得很慢的SQL語句,你可能會問慢SQL會有啥問題嗎?
試想一個場景:
大白和小黑端午出去玩,機票太貴于是買了高鐵,火車站的人真是烏央烏央的。 馬上檢票了,大白和小黑準備去廁所清理下庫存,坑位不多,排隊的人還真不少。 小黑發現其中有3個坑的乘客賊慢,其他2個坑位換了好幾波人,這3位坑主就是不出來。 等在外面的大夥,心裡很是不爽,長期占用公共資源,後面的人沒法用。 小黑苦笑道:這不就是廁所版的慢SQL嘛!
這是實際生活中的例子,換到MySQL伺服器也是一樣的,畢竟科技源自生活嘛。
MySQL伺服器的資源(CPU、IO、記憶體等)是有限的,尤其在高并發場景下需要快速處理掉請求,否則一旦出現慢SQL就會阻塞掉很多正常的請求,造成大面積的失敗/逾時等。
用戶端和MySQL服務端的互動過程簡介:
用戶端發送一條SQL語句給服務端,服務端的連接配接器先進行賬号/密碼、權限等環節驗證,有異常直接拒絕請求。
服務端查詢緩存,如果SQL語句命中了緩存,則傳回緩存中的結果,否則繼續處理。
服務端對SQL語句進行詞法解析、文法解析、預處理來檢查SQL語句的合法性。
服務端通過優化器對之前生成的解析樹進行優化處理,生成最優的實體執行計劃。
将生成的實體執行計劃調用存儲引擎的相關接口,進行資料查詢和處理。
處理完成後将結果傳回用戶端。
用戶端和MySQL服務端的互動過程簡圖:
俗話說"條條大路通羅馬",優化器的作用就是找到這麼多路中最優的那一條。
存儲引擎更是決定SQL執行的核心元件,适當了解其中原理十分有益。
InnoDB存儲引擎(Storage Engine)是MySQL預設之選,是以非常典型。
存儲引擎的主要作用是進行資料的存取和檢索,也是真正執行SQL語句的元件。
InnoDB的整體架構分為兩個部分:記憶體架構和磁盤架構,如圖:
存儲引擎的内容非常多,并不是一篇文章能說清楚的,本文不過多展開,我們在此隻需要了解記憶體架構和磁盤架構的大緻組成即可。
InnoDB 引擎是面向行存儲的,資料都是存儲在磁盤的資料頁中,資料頁裡面按照固定的行格式存儲着每一行資料。
行格式主要分為四種類型Compact、Redundant、Dynamic和Compressed,預設為Compact格式。
當計算機通路一個資料時,不僅會加載目前資料所在的資料頁,還會将目前資料頁相鄰的資料頁一同加載到記憶體,磁盤預讀的長度一般為頁的整倍數,進而有效降低磁盤IO的次數。
MySQL中磁盤的資料需要被交換到記憶體,才能完成一次SQL互動,大緻如圖:
扇區是硬碟的讀寫的基本機關,通常情況下QQ賬号拍賣地圖每個扇區的大小是 512B
磁盤塊檔案系統讀寫資料的最小機關,相鄰的扇區組合在一起形成一個塊,一般是4KB
頁是記憶體的最小存儲機關,頁的大小通常為磁盤塊大小的 2^n 倍
InnoDB頁面的預設大小是16KB,是數倍個作業系統的頁
MySQL的資料是一行行存儲在磁盤上的,并且這些資料并非實體連續地存儲,這樣的話要查找資料就無法避免随機在磁盤上讀取和寫入資料。
對于MySQL來說,當出現大量磁盤随機IO時,大部分時間都被浪費到尋道上,磁盤呼噜呼噜轉,就是傳輸不了多少資料。
一次磁盤通路由三個動作組成: 尋道:磁頭移動定位到指定磁道 旋轉:等待指定扇區從磁頭下旋轉經過 資料傳輸:資料在磁盤與記憶體之間的實際傳輸
對于存儲引擎來說,如何有效降低随機IO是個非常重要的問題。
可以實作增删改查的資料結構非常多,包括:哈希表、二叉搜尋樹、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加載到記憶體
将上一步獲得的資料,在記憶體中進行二分查找獲得全部行資料
上述查詢就包含了一次回表過程,是以性能比主鍵查詢慢了一倍,是以盡量使用主鍵查詢,一次完事。
出現慢SQL的原因很多,我們抛開單表數億記錄和無索引的特殊情況,來讨論一些更有普遍意義的慢SQL原因和解決之道。
我們從兩個方面來進行闡述:
資料庫表索引設定不合理
SQL語句有問題,需要優化
程式員的角度和存儲引擎的角度是不一樣的,索引寫的好,SQL跑得快。
索引區分度低
假如表中有1000w記錄,其中有status字段表示狀态,可能90%的資料status=1,可以不将status作為索引,因為其對資料記錄區分度很低。
切忌過多建立索引
每個索引都需要占用磁盤空間,修改表資料時會對索引進行更新,索引越多,更新越複雜。
因為每添加一個索引,.ibd檔案中就需要多元護一個B+Tree索引樹,如果某一個table中存在10個索引,那麼就需要維護10棵B+Tree,寫入效率會降低,并且會浪費磁盤空間。
常用查詢字段建索引
如果某個字段經常用來做查詢條件,那麼該字段的查詢速度會影響整個表的查詢速度,屬于熱門字段,為其建立索引非常必要。
常排序/分組/去重字段建索引
對于需要經常使用ORDER BY、GROUP BY、DISTINCT和UNION等操作的字段建立索引,可以有效借助B+樹的特性來加速執行。
主鍵和外鍵建索引
主鍵可以用來建立聚集索引,外鍵也是唯一的且常用于表關聯的字段,也需要建索引來提高性能。
如果資料庫表的索引設定比較合理,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子句,可以将過濾資料多的條件放在前面,最快速度縮小結果集。
使用小範圍事務,而非大範圍事務
遵循最左比對原則
使用聯合索引,而非建立多個單獨索引
在分析慢SQL之前需要通過MySQL進行相關設定:
開啟慢SQL日志
設定慢SQL的執行時間門檻值
explain指令隻需要加在select之前即可,例如:
explain select * from std where id < 100;
該指令會展示sql語句的詳細執行過程,幫助我們定位問題,網上關于explain的用法和講解很多,本文不再展開。
本文從慢SQL的危害、Innodb存儲引擎、聚集索引、非聚集索引、索引失效、SQL優化、慢SQL分析等角度進行了闡述。
MySQL的很多知識點都非常複雜,并非一兩篇文章能講清楚的,是以本文在很多地方顯得很單薄,好在網上資料非常多。
如果本文能在某些方面對讀者有所啟發,足矣。