MySQL查詢性能優化(精)

MySQL查詢性能優化

  MySQL查詢性能的優化涉及多個方面,其中包括庫表結構、創建合理的索引、設計合理的查詢。庫表結構包括如何設計表之間的關聯、表字段的數據類型等。這須要依據具體的場景進行設計。以下咱們從數據庫的索引和查詢語句的設計兩個角度介紹如何提升MySQL查詢性能。sql

數據庫索引

  索引是存儲引擎中用於快速找到記錄的一種數據結構。索引有多種分類方式,按照存儲方式能夠分爲:聚簇索引和非聚簇索引;按照數據的惟一性能夠分爲:惟一索引和非惟一索引;按照列個數能夠分爲:單列索引和多列索引等。索引也有多種類型: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列上創建了索引。性能優化

  1. 全值匹配:指和索引中的全部列進行匹配,例如查找name='Jone' and age=13 and sex='男'的人;
  2. 匹配最左前綴:指用索引的第一列name,如where name='Jone',該查詢只使用了索引的第一列
  3. 匹配列前綴:匹配索引列值的開頭,如where name like 'J%',查找名字以J開頭的人;
  4. 匹配範圍值:例如查找年齡在10-30之間的Jone,where name='Jone' and age between 10 and 30;
  5. 只訪問索引的查詢:若是在select中選擇的字段都是索引中的字段,那麼就不須要訪問數據行,從而提升查詢速度。
  6. 若是不是按照索引的最左列進行查找,則沒法使用索引,如當僅查找表A中年齡爲15歲的人時則沒法使用索引;
  7. 不能跳過索引中的列,如查找表A中名字爲Jone且爲男性的人,則索引只能使用name列,沒法使用sex列;
  8. 查詢中索引的某列是範圍查詢,則該列後的查詢條件將不能使用索引。

Hash索引與B-Tree的區別:服務器

  1. Hash索引指包含哈希值(根據key中的列計算)和行指針,而B-Tree存儲的是列值。因此Hash不能使用索引來避免讀取數據行;
  2. Hash索引數據不是按照索引值順序存儲的,因此沒法用於排序;
  3. Hash索引不支持部分索引列匹配查找,由於Hash值是根據索引中的所有列計算出來的;
  4. Hash索引只支持等值比較查詢,包括=、in()、<=>。不支持範圍查詢。
  • 索引的優勢

索引不只僅可讓服務器快速定位到表的指定位置,並且還有如下優勢:網絡

  1. B-Tree索引按照列的順序存儲數據,因此能夠用來作Order by和group by操做,避免排序和臨時表
  2. B-Tree索引中存儲索引列的值,因此當select的值在索引中時,能夠避免訪問數據行
  3. 索引能夠有效減小服務器掃描的數據量。
  • 高性能的索引策略

  正確地建立和使用索引是實現高性能查詢的基礎。前面已經介紹了各類類型的索引以及對應的優缺點。高效地選擇和使用索引有不少種方式,其中有些是針對特殊案例的優化,有些則是針對特定行爲的優化。數據結構

  1. 獨立的列:指索引不能是表達式的一部分,也不能是函數的參數。如:select * from A where id+1=5; 則沒法使用主鍵索引。
  2. 前綴索引和索引選擇性:有時須要索引很長的字符串,索引會佔用很大的空間,一般能夠索引開始的部分字符來節約索引空間,提升索引效率,但也會下降索引的選擇性。索引的選擇性=不重複索引值/數據表的記錄總數。索引的選擇性越高查詢效率越高。
  3. 多列索引:首先須要說明在多列上建立索引不等同於給這些列的每一列單獨創建索引。當執行查詢的時候,MySQL只能使用一個索引。若是你有三個單列的索引,MySQL會試圖選擇一個限制最嚴格的索引。即便是限制最嚴格的單列索引,它的限制能力也確定遠遠低於這三個列上的多列索引。好比咱們想查詢表A中id爲3或者名字首字母爲A的人,sql語句的兩種寫法對比,其中第二種寫法比第一種減小對錶的掃描次數:
  4. 多列索引中索引列的順序也十分重要,在設計索引的順序時也須要考慮如何更好地知足排序和分組的須要(B-Tree)。在一個多列的B-Tree索引中,索引列的順序意味着索引首先按照最左列進行排序,其次是第二列等等。肯定索引列的順序有一個經驗法則:將選擇性最高的列放到索引最前列。固然若是須要考慮對錶的排序的狀況就須要另當考慮了。
  5. 聚簇索引:不是一種單獨的索引類型,而是一種數據存儲方式,具體的細節依賴於其實現方式,InnoDB的聚簇索引實際上在同一個結構中保存了B-Tree索引和數據行,一個表只能有一個聚簇索引。聚簇索引的優(1-3)缺(4-7)點以下:
    1. 能夠把相關數據保存在一塊兒。例如實現電子郵箱時,能夠根據用戶ID來彙集數據,這樣只須要從磁盤讀取少數的數據頁就可以獲取某個用戶的所有郵件。若是沒有聚簇索引,則每封郵件均可能致使一次磁盤I/O;
    2. 數據訪問更快。聚簇索引將索引和數據保存在同一個B-Tree中,所以聚簇索引中獲取數據一般比在非聚簇索引中查找要快;
    3. 使用覆蓋索引掃描的查詢能夠直接使用頁節點中的主鍵值;
    4. B-Tree索引插入速度嚴重依賴於插入順序。按照聚簇索引列中值的順序插入數據到InnoDB表中速度最快的;
    5. 更新聚簇索引列的值代價很高,由於會強制InnoDB將每一個被更新列所在的行移動到新的位置;
    6. 插入新的行可能面臨「頁分裂」的問題。頁分裂問題是聚簇索引要求必須將這一行插入到某個已滿的頁中時,存儲引擎會將該頁分裂成兩個頁面來容納該行,也就是一次頁分裂操做,致使表佔用更多的磁盤空間;
    7. 聚簇索引可能致使全表掃描變慢,尤爲是行比較稀疏,或者因爲頁分裂致使數據存儲不連續的時候。

      如上是盜取的一個向InnoDB表中插入數據的時間和索引大小的圖,其中userinfo表和userinfo_uuid表惟一的區別是userinfo表以id爲主鍵,而userinfo_uuid表以uuid爲主鍵,而插入100萬和300萬數據的順序是按照id列的順序插入的,由上圖可知,當插入300萬數據行時,userinfo_uuid表因爲不是按照主鍵(uuid)的順序插入的數據,會致使大量的頁分裂,從而插入須要更多的時間、索引佔用更大的空間。函數

  6. 覆蓋索引:你們都會根據where的條件創建合適的索引,這只是索引優化的一個方面。優秀的索引還應該考慮整個查詢。MySQL可使用索引直接獲取列的數據,這樣就不須要讀取數據行了。若是索引包含(覆蓋)全部須要查詢的字段值,咱們就稱之爲覆蓋索引。當查詢是一個索引覆蓋查詢時,Extra列能夠看到Using index的信息。

    固然覆蓋查詢仍是有不少陷阱可能致使沒法實現優化的。MySQL查詢優化器會在執行查詢前判斷是否有一個索引可以進行覆蓋,覆蓋where條件中的字段和select的字段。若是不能覆蓋,則仍是須要掃描數據行。post

    由於InnoDB表中非聚簇索引中存儲主鍵值,因此咱們先根據條件獲取主鍵值,而後再根據主鍵值進行查詢,這種方式叫作延遲關聯。性能

  7. 使用索引掃描來作排序。若是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是否在掃描額外的記錄,其中能夠經過掃描行數和返回行數進行衡量。若是發現查詢中須要掃描大量的數據可是隻返回少數的行,一般能夠:

  1. 使用索引覆蓋掃描,把全部須要的列都放入索引,這樣存儲引擎無須回表獲取對應行就能夠返回結果;
  2. 改變庫表結構;
  3. 重寫這個複雜的查詢,讓MySQL優化器可以以更優的方式執行這個查詢。
  • 重構查詢方式

  設計查詢的時候一個須要考慮的重要問題是,是否須要將一個複雜的查詢分紅多個簡單的查詢。在傳統的實現中老是強調數據庫層完成儘量多的工做,這樣的邏輯在於之前老是認爲網絡通訊、查詢解析和優化是一件代價很高的事情。可是這樣的想法對於MySQL並不適用,MySQL從設計上鍊接和斷開鏈接都很輕量級,在返回一個小的查詢結果方面很高效。

  分解關聯查詢:不少高性能的應用都會對關聯查詢進行分解,簡單地說就是對每一個表進行一次單表查詢,而後將結果在應用程序中進行關聯。以下圖所示:

  查詢計算機1班學生的全部成績,咱們能夠將上過程分解爲三個子步驟,以下:

  那麼這麼分解的好處又在哪裏呢?首先是讓緩存的效率更高。許多應用程序能夠方便的緩存單表查詢對應的結果對象。如已經緩存了計算機1班對應的id爲1,tb_student表中1班的學生有1號和5號,從而能夠從成績表中查詢1號和5號學生的成績;其次查詢分解後,執行單個查詢能夠減小鎖競爭;再次查詢自己效率也會有所提高。如上使用in()代替關聯查詢,可讓MySQL按照ID順序進行查詢,這可能比隨機的關聯更加高效;最後分解關聯查詢能夠減小冗餘記錄的查詢,在應用層作關聯查詢時,意味着對於某條記錄應用只須要查詢一次,而在數據庫中作關聯查詢,則可能須要重複地訪問一部分數據。

  • 查詢執行的基礎

  當但願MySQL可以以較高的性能運行查詢時,最好的辦法就是弄清楚MySQL是如何優化和執行查詢的。以下圖展現了向MySQL發送一個請求時MySQL具體的操做過程:

  1. 首先服務器接收到一條客戶端請求,先檢查查詢緩存,若是命中緩存,則馬上返回緩存中的數據,不然進入下一階段;
  2. 服務器進行SQL解析、預處理,再由優化器生成對應的執行計劃;
  3. MySQL根據優化器生成的執行計劃,調用存儲引擎的API執行查詢;
  4. 將結果返回給客戶端。

  第一步是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可以處理的優化類型:

  1. 從新定義關聯表的順序:數據表的關聯並不老是按照查詢中指定的順序進行。
  2. 將外鏈接轉化爲內鏈接:並非OUTER JOIN語句都必須之外鏈接的方式執行。如where條件、庫表結構均可能會讓外鏈接等價於一個內鏈接;
  3. 使用等價變換:MySQL使用等價變換來規範表達式。如(a<b and b=c) and a=10則會改寫爲a=10 and b>10 and b=c;
  4. 優化count()、min()、max()
  5. 覆蓋索引掃描:當索引中的列包含所須要的列時,MySQL使用索引返回須要的數據,不須要查詢對應的行數據;
  6. 子查詢優化:將子查詢轉化一種效率更高的形式,從而減小多個查詢屢次對數據的訪問;
  7. 提早終止查詢:使用limit時,發現已經知足查詢需求時,MySQL可以馬上終止查詢;
  8. 列表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協議進行傳輸,從而是客戶端能夠在第一時間得到返回的結果。

  • 優化特定類型的查詢
  1. 優化count()查詢。若是指定了列,則查詢該列不爲null的行數,若是爲count(*)則查詢總行數。
  2. 優化關聯查詢,確保on或者using子句中的列上有索引。確保group by和order by的表達式只涉及一個表中的列,這樣MySQL纔有可能使用索引來優化整個過程。
  3. 優化group by和distinct。MySQL使用一樣的方法優化這兩類查詢,一般是利用索引的順序性進行優化。可是若是沒法使用索引,group by使用兩種策略來完成:使用臨時表或者文件排序來作分組。
  4. 優化limit分頁,使用延遲關聯的方式來優化limit分頁;
  5. 優化UNION查詢。MySQL經過建立並填充臨時表的方式來執行UNION查詢,所以須要手工的將where、limit、order by等子句「下推」到UNION的各個子查詢中,除非確實須要服務器消除重複的行,不然必定要使用UNION ALL,若是沒有ALL關鍵字,MySQL會給臨時表加上distinct,從而對臨時表的數據作惟一性檢查,這樣代價很是高。
  • 總結

  綜上全部的內容可知,建立高性能應用程序要考慮schema、索引、查詢語句以及查詢優化等問題。理解查詢是如何被執行的以及時間都消耗在哪些地方,從而針對耗時大的查詢語句進行改進。

相關文章
相關標籤/搜索