170727、MySQL查詢性能優化
MySQL查詢性能優化
MySQL查詢性能的優化涉及多個方面,其中包括庫表結構、建立合理的索引、設計合理的查詢。庫表結構包括如何設計表之間的關聯、表字段的數據類型等。這需要依據具體的場景進行設計。如下我們從數據庫的索引和查詢語句的設計兩個角度介紹如何提高MySQL查詢性能。
數據庫索引
索引是存儲引擎中用於快速找到記錄的一種數據結構。索引有多種分類方式,按照存儲方式可以分為:聚簇索引和非聚簇索引;按照數據的唯一性可以分為:唯一索引和非唯一索引;按照列個數可以分為:單列索引和多列索引等。索引也有多種類型:B-Tree索引、Hash索引、空間數據索引(R-Tree)、全文索引等。
-
B-Tree索引
在利用B-Tree索引進行查詢的過程中,有幾點註意事項,我們以表A進行說明。其中表A的定義如下:
create table A(id int auto_increment primary key, name varchar(10), age tinyint, sex enum(‘男‘,‘女‘), birth datatime, key(name,age,sex)); id為主鍵,並在name,age,sex列上建立了索引。
-
全值匹配:指和索引中的所有列進行匹配,例如查找name=‘Jone‘ and age=13 and sex=‘男‘的人;
-
匹配最左前綴:指用索引的第一列name,如where name=‘Jone‘,該查詢只使用了索引的第一列
-
匹配列前綴:匹配索引列值的開頭,如where name like ‘J%‘,查找名字以J開頭的人;
-
匹配範圍值:例如查找年齡在10-30之間的Jone,where name=‘Jone‘ and age between 10 and 30;
-
只訪問索引的查詢:如果在select中選擇的字段都是索引中的字段,那麽就不需要訪問數據行,從而提高查詢速度。
-
如果不是按照索引的最左列進行查找,則無法使用索引,如當僅查找表A中年齡為15歲的人時則無法使用索引;
-
不能跳過索引中的列,如查找表A中名字為Jone且為男性的人,則索引只能使用name列,無法使用sex列;
-
查詢中索引的某列是範圍查詢,則該列後的查詢條件將不能使用索引。
Hash索引與B-Tree的區別:
-
Hash索引指包含哈希值(根據key中的列計算)和行指針,而B-Tree存儲的是列值。所以Hash不能使用索引來避免讀取數據行;
-
Hash索引數據不是按照索引值順序存儲的,所以無法用於排序;
-
Hash索引不支持部分索引列匹配查找,因為Hash值是根據索引中的全部列計算出來的;
-
Hash索引只支持等值比較查詢,包括=、in()、<=>。不支持範圍查詢。
-
索引的優點
索引不僅僅可以讓服務器快速定位到表的指定位置,而且還有以下優點:
-
B-Tree索引按照列的順序存儲數據,所以可以用來做Order by和group by操作,避免排序和臨時表
-
B-Tree索引中存儲索引列的值,所以當select的值在索引中時,可以避免訪問數據行
-
索引可以有效減少服務器掃描的數據量。
-
高性能的索引策略
正確地創建和使用索引是實現高性能查詢的基礎。前面已經介紹了各種類型的索引以及對應的優缺點。高效地選擇和使用索引有很多種方式,其中有些是針對特殊案例的優化,有些則是針對特定行為的優化。
-
獨立的列:指索引不能是表達式的一部分,也不能是函數的參數。如:select * from A where id+1=5; 則無法使用主鍵索引。
-
前綴索引和索引選擇性:有時需要索引很長的字符串,索引會占用很大的空間,通常可以索引開始的部分字符來節約索引空間,提高索引效率,但也會降低索引的選擇性。索引的選擇性=不重復索引值/數據表的記錄總數。索引的選擇性越高查詢效率越高。
-
多列索引:首先需要說明在多列上創建索引不等同於給這些列的每一列單獨建立索引。當執行查詢的時候,MySQL只能使用一個索引。如果你有三個單列的索引,MySQL會試圖選擇一個限制最嚴格的索引。即使是限制最嚴格的單列索引,它的限制能力也肯定遠遠低於這三個列上的多列索引。比如我們想查詢表A中id為3或者名字首字母為A的人,sql語句的兩種寫法對比,其中第二種寫法比第一種減少對表的掃描次數:
-
多列索引中索引列的順序也十分重要,在設計索引的順序時也需要考慮如何更好地滿足排序和分組的需要(B-Tree)。在一個多列的B-Tree索引中,索引列的順序意味著索引首先按照最左列進行排序,其次是第二列等等。確定索引列的順序有一個經驗法則:將選擇性最高的列放到索引最前列。當然如果需要考慮對表的排序的情況就需要另當考慮了。
-
聚簇索引:不是一種單獨的索引類型,而是一種數據存儲方式,具體的細節依賴於其實現方式,InnoDB的聚簇索引實際上在同一個結構中保存了B-Tree索引和數據行,一個表只能有一個聚簇索引。聚簇索引的優(1-3)缺(4-7)點如下:
-
可以把相關數據保存在一起。例如實現電子郵箱時,可以根據用戶ID來聚集數據,這樣只需要從磁盤讀取少數的數據頁就能夠獲取某個用戶的全部郵件。如果沒有聚簇索引,則每封郵件都可能導致一次磁盤I/O;
-
數據訪問更快。聚簇索引將索引和數據保存在同一個B-Tree中,因此聚簇索引中獲取數據通常比在非聚簇索引中查找要快;
-
使用覆蓋索引掃描的查詢可以直接使用頁節點中的主鍵值;
-
B-Tree索引插入速度嚴重依賴於插入順序。按照聚簇索引列中值的順序插入是加載數據到InnoDB表中速度最快的;
-
更新聚簇索引列的代價很高,因為會強制InnoDB將每個被更新的行移動到新的位置;
-
被插入的新行在移動時,可能面臨“頁分裂”的問題。頁分裂問題是聚簇索引要求必須將這一行插入到某個已滿的頁中時,存儲引擎會將該頁分裂成兩個頁面來容納該行,也就是一次頁分裂操作,導致表占用更多的磁盤空間;
-
聚簇索引可能導致全表掃描變慢,尤其是行比較稀疏,或者由於頁分裂導致數據存儲不連續的時候。
如上是盜取的一個向InnoDB表中插入數據的時間和索引大小的圖,其中userinfo表和userinfo_uuid表唯一的區別是userinfo表以id為主鍵,而userinfo_uuid表以uuid為主鍵,而插入100萬和300萬數據的順序是按照id列的順序插入的,由上圖可知,當插入300萬數據行時,userinfo_uuid表由於不是按照主鍵(uuid)的順序插入的數據,會導致大量的頁分裂,從而插入需要更多的時間、索引占用更大的空間。
-
覆蓋索引:大家都會根據where的條件建立合適的索引,這只是索引優化的一個方面。優秀的索引還應該考慮整個查詢。MySQL可以使用索引直接獲取列的數據,這樣就不需要讀取數據行了。如果索引包含(覆蓋)所有需要查詢的字段值,我們就稱之為覆蓋索引。當查詢是一個索引覆蓋查詢時,Extra列可以看到Using index的信息。
當然覆蓋查詢還是有很多陷阱可能導致無法實現優化的。MySQL查詢優化器會在執行查詢前判斷是否有一個索引能夠進行覆蓋,覆蓋where條件中的字段和select的字段。如果不能覆蓋,則還是需要掃描數據行。
因為InnoDB表中非聚簇索引中存儲主鍵值,所以我們先根據條件獲取主鍵值,然後再根據主鍵值進行查詢,這種方式叫做延遲關聯。
-
使用索引掃描來做排序。如果EXPLAIN出來的type列值為index,說明MySQL使用了索引掃描來做排序。掃描索引本身是很快的,但是如果索引不能覆蓋查詢所需的全部列,那就不得不每掃描一條索引記錄就回表查詢一次對應的行。這基本都是隨機I/O,因此按索引順序讀取的速度通常要比順序地全表掃描慢,尤其是I/O密集型的工作負載時。因此MySQL設計索引時應盡可能的滿足排序和查找。只有索引列順序和order by子句的順序完全一致,並且所有列的排序方向都一致時,MySQL才能使用索引來對結果做排序。如果查詢關聯多張表,則只有order by子句引用的字段全部為第一個表時,才能使用索引排序。
如上是分別使用主鍵id排序和name排序的查詢,可以看出使用id排序的查詢使用了索引排序,而name排序的查詢使用的是filesort。
-
總結
總的來說編寫查詢語句時,應盡可能選擇合適的索引以避免單行查找,盡可能的使用原生順序從而避免額外的排序操作,並盡可能使用索引覆蓋查詢。我們通過響應時間來對查詢進行分析,找出消耗時間最長的查詢或者給服務器帶來壓力最大的查詢,然後檢查查詢的schema、SQL和索引結構,判斷是否有查詢掃描了太多的行,是否做了很多額外的排序或者使用了臨時表,是否使用了隨機I/O訪問數據,或者太多回表查詢哪些不在索引中的列的操作。
查詢設計
在發現查詢效率不高時,首先就需要考慮查詢語句的設計是否合理。如下將會介紹一些查詢優化技巧,然後在介紹一些MySQL優化器內部的機制,並展示MySQL是如何執行查詢的。最後探索查詢優化的模式,以幫助MySQL更有效地執行查詢。
-
優化數據訪問
查詢性能低下的最基本原因是訪問的數據太多了。因此大部分的性能低下查詢都可以通過減少訪問的數據量進行優化。減少數據訪問量通常意味著訪問了太多的行,但有時也可能是訪問了太多的列。在查詢時如果僅需要查詢結果集中的前某些行,則最簡單的方式是在查詢語句的最後加上limit。在進行多表關聯查詢時應盡量避免使用select *,因為它返回表的所有列,但是這些列可能並不都是必須的。除了請求了不需要的數據,還需要查看MySQL是否在掃描額外的記錄,其中可以通過掃描行數和返回行數進行衡量。如果發現查詢中需要掃描大量的數據但是只返回少數的行,通常可以:
-
使用索引覆蓋掃描,把所有需要的列都放入索引,這樣存儲引擎無須回表獲取對應行就可以返回結果;
-
改變庫表結構;
-
重寫這個復雜的查詢,讓MySQL優化器能夠以更優的方式執行這個查詢。
-
重構查詢方式
設計查詢的時候一個需要考慮的重要問題是,是否需要將一個復雜的查詢分成多個簡單的查詢。在傳統的實現中總是強調數據庫層完成盡可能多的工作,這樣的邏輯在於以前總是認為網絡通信、查詢解析和優化是一件代價很高的事情。但是這樣的想法對於MySQL並不適用,MySQL從設計上連接和斷開連接都很輕量級,在返回一個小的查詢結果方面很高效。
分解關聯查詢:很多高性能的應用都會對關聯查詢進行分解,簡單地說就是對每個表進行一次單表查詢,然後將結果在應用程序中進行關聯。如下圖所示:
查詢計算機1班學生的所有成績,我們可以將上過程分解為三個子步驟,如下:
那麽這麽分解的好處又在哪裏呢?首先是讓緩存的效率更高。許多應用程序可以方便的緩存單表查詢對應的結果對象。如已經緩存了計算機1班對應的id為1,tb_student表中1班的學生有1號和5號,從而可以從成績表中查詢1號和5號學生的成績;其次查詢分解後,執行單個查詢可以減少鎖競爭;再次查詢本身效率也會有所提升。如上使用in()代替關聯查詢,可以讓MySQL按照ID順序進行查詢,這可能比隨機的關聯更加高效;最後分解關聯查詢可以減少冗余記錄的查詢,在應用層做關聯查詢時,意味著對於某條記錄應用只需要查詢一次,而在數據庫中做關聯查詢,則可能需要重復地訪問一部分數據。
-
查詢執行的基礎
當希望MySQL能夠以較高的性能運行查詢時,最好的辦法就是弄清楚MySQL是如何優化和執行查詢的。如下圖展示了向MySQL發送一個請求時MySQL具體的操作過程:
-
首先服務器接收到一條客戶端請求,先檢查查詢緩存,如果命中緩存,則立刻返回緩存中的數據,否則進入下一階段;
-
服務器進行SQL解析、預處理,再由優化器生成對應的執行計劃;
-
MySQL根據優化器生成的執行計劃,調用存儲引擎的API執行查詢;
-
將結果返回給客戶端。
第一步是MySQL客戶端/服務器通信,二者之間通信協議是“半雙工”的,也就是說在某一時刻只能有一方在發送數據。在任何一個時刻MySQL連接都有一個狀態,該狀態表示MySQL當前的工作,通過SHOW FULL PROCESSLIST命令查詢狀態。其中狀態有Sleep、Query、Locked、Analyzing and statistics、Coping to tmp table、Sorting result、Sending data。
第二步是查尋緩存。在解析一個查詢語句之前,如果查詢緩存是打開的,那麽MySQL會優先檢查這個查詢是否命中查詢緩存中的數據。通常是通過一個對大小寫敏感的Hash查找實現。如果命中,那麽在返回結果前MySQL會檢查一次用戶權限,該過程無須解析查詢SQL語句。如果未命中,則解析SQL語句。
第三步是查詢優化處理。包括解析SQL、預處理、優化SQL執行計劃,其中出現任何錯誤都會終止查詢。首先,MySQL通過關鍵字將SQL語句進行解析,並生成一棵對應的“解析樹”。查詢優化器負責將解析樹轉化成執行計劃,優化器的作用就是找到查詢的較優執行計劃。MySQL使用基於成本的優化器,它將嘗試預測一個查詢使用某種執行計劃時的成本(SHOW STATUS LIKE ‘Last_query_cost‘),並選擇成本最小的一個。查詢優化器是一個非常復雜的部件,它使用了很多優化策略來生成一個最優的執行計劃。優化策略分為:靜態優化和動態優化。靜態優化可以直接對解析樹進行分析,並完成優化。例如,優化器可以通過簡單的代數變換將where條件轉換成另一種等價形式,靜態優化不依賴於特別的數值,如where中帶入的常數。靜態優化在第一次完成後就一直有效,即使使用不同的參數重復執行也不會發生變化,可以認為是一種“編譯時優化”。動態優化是上下文相關的,如where條件中取值、索引條目對應的數據行數等,是一種“運行時優化”。如下是MySQL能夠處理的優化類型:
-
重新定義關聯表的順序:數據表的關聯並不總是按照查詢中指定的順序進行。
-
將外連接轉化為內連接:並不是OUTER JOIN語句都必須以外連接的方式執行。如where條件、庫表結構都可能會讓外連接等價於一個內連接;
-
使用等價變換:MySQL使用等價變換來規範表達式。如(a<b and b=c) and a=10則會改寫為a=10 and b>10 and b=c;
-
優化count()、min()、max()
-
覆蓋索引掃描:當索引中的列包含所需要的列時,MySQL使用索引返回需要的數據,不需要查詢對應的行數據;
-
子查詢優化:將子查詢轉化一種效率更高的形式,從而減少多個查詢多次對數據的訪問;
-
提前終止查詢:使用limit時,發現已經滿足查詢需求時,MySQL能夠立刻終止查詢;
-
列表in()比較:MySQL中in()不等同於多個or條件的子句,因為MySQL首先對in()中的數據進行排序,然後通過二分查找的方式來確定列表中的值是否滿足條件,該時間復雜度為o(logn),而多個or查詢的時間復雜度為o(n)。
當MySQL需要對選擇的數據進行排序時,如果無法使用索引進行排序,那麽MySQL在數據量小則在內存中進行排序,如果數據量大則需要磁盤進行排序,不過MySQL將這一過程統一稱為文件排序(filesort)。如果需要排序的數據量小於“排序緩沖區”,MySQL使用內存進行“快速排序”操作,如果內存不夠排序,MySQL先對數據進行分塊,然後對每個獨立的塊使用“快速排序”,並將各塊排序結果放入磁盤,然後將各個排好序的塊進行合並(merge)。在關聯查詢的時候如果需要排序,MySQL會分兩種情況來處理這樣的文件排序,如果order by子句中的所有列都來自關聯的第一個表,那麽MySQL在關聯處理第一個表的時候就進行文件排序,則MySQL的EXPLAIN結果的extra字段就會有“using filesort”。除此之外的其他情況,MySQL都會先將關聯結果放到一個臨時表中,,然後在所有關聯都結束後再進行文件排序,此時的MySQL的EXPLAIN結果的extra字段值為“Using temporary;Using filesort”。如果查詢中有limit的話,limit也會在排序之後應用,所以即使返回較少的數據,臨時表和需要排序的數量仍會非常大(MySQL5.6的limit子句在此處已經做了改進)。
第四步是查詢執行引擎。MySQL根據執行計劃給出的指令逐步執行,在該過程中,有大量的操作需要通過調用存儲引擎實現的接口來完成,也就是“Handler API”。MySQL在優化階段就為每個表創建一個handler實例,優化器根據這些實例的接口獲取表的相關信息。
最後一步就是將查詢的結果返回給客戶端。MySQL將結果集返回客戶端是一個增量、逐步返回的過程。一旦服務器處理完最後一個關聯表,開始生成第一條結果時,MySQL就可以開始想客戶端逐步返回結果。這樣有兩個好處:一是服務器端無須存儲太多的結果;二是結果集中的每一行都會以一個滿足MySQL客戶端/服務器通信協議的封包發送,再通過TCP協議進行傳輸,從而是客戶端可以在第一時間獲得返回的結果。
-
優化特定類型的查詢
-
優化count()查詢。如果指定了列,則查詢該列不為null的行數,如果為count(*)則查詢總行數。
-
優化關聯查詢,確保on或者using子句中的列上有索引。確保group by和order by的表達式只涉及一個表中的列,這樣MySQL才有可能使用索引來優化整個過程。
-
優化group by和distinct。MySQL使用同樣的方法優化這兩類查詢,通常是利用索引的順序性進行優化。但是如果無法使用索引,group by使用兩種策略來完成:使用臨時表或者文件排序來做分組。
-
優化limit分頁,使用延遲關聯的方式來優化limit分頁;
-
優化UNION查詢。MySQL通過創建並填充臨時表的方式來執行UNION查詢,因此需要手工的將where、limit、order by等子句“下推”到UNION的各個子查詢中,除非確實需要服務器消除重復的行,否則一定要使用UNION ALL,如果沒有ALL關鍵字,MySQL會給臨時表加上distinct,從而對臨時表的數據做唯一性檢查,這樣代價非常高。
-
總結
綜上所有的內容可知,創建高性能應用程序要考慮schema、索引、查詢語句以及查詢優化等問題。理解查詢是如何被執行的以及時間都消耗在哪些地方,從而針對耗時大的查詢語句進行改進。
170727、MySQL查詢性能優化