查詢性能優化 深入理解MySql如何執行查詢

本篇深入瞭解查詢優化和服務器的內部機制,瞭解MySql如何執行特定查詢,從中也可以知道如何更改查詢執行計劃,當我們深入理解MySql如何真正地執行查詢,明白高效和低效的真正含義,在實際應用中就能揚長避短。

聲明:本人使用的數據庫版本爲MySql 5.1

 

回到頂部

一、基本原則:優化數據訪問

查詢性能低下的最基本原因就是訪問了太多數據,一些查詢要不可避免地篩選大量的數據,大部分性能欠佳的查詢都可以用減少數據訪問的方式進行優化。

1、首先分析應用程序是否正在獲取超過需要的數據,這通常表現在獲取了過多的行或列。一些查詢先向服務器請求不需要的數據,再丟掉他們,這個讓服務器造成了額外的負擔,增加了網絡開銷,消耗了內存和CPU資源。

  > 如果前臺只需要顯示15條數據,而你的查詢結果集返回了100條,則要想想是否真有必要這樣幹了,最好使用LIMIT來限制查詢的條數。

  > 儘量避免使用SELECT * , 也許你並不需要所有的列,但獲取所有的列將會造成覆蓋索引這樣的優化手段失效,也會增加磁盤I/O、內存和CPU的開銷等,所以基於這種情況,儘量使用SELECT t.id, t.name ... 這種查詢具體字段的SQL。

     但是,SELECT * 這種稍顯浪費的方式可以簡化開發,增加代碼的複用性(比如以後擴展了字段,就不用再改sql代碼了)。

     如果系統使用了持久化框架,而我們只查詢了某一些字段出來,然後再直接去更新這個持久化對象時,那些未查詢出來的字段就會被設置爲NULL,導致數據丟失。所以,如果只查詢一部分字段,要避免去更新持久化對象(親身經歷)。

   在程序中,還是倡導使用SELECT t.id, t.name ... 這種形式,能更好地利用索引;如果只是顯示數據,那就按需查詢部分字段即可,這樣能更充分利用覆蓋索引;如果需要更新數據,則必須查詢出所有字段。

2、其次看是否檢查了過多的數據,一般從查詢的執行時間、檢查的行數、返回的行數來看,但這些不可作爲絕對的標準。

  > 看下面的這個執行計劃:

    第一幅圖中:key表明使用了id_card索引;rows=1,表明只檢查了一行數據,所以其速度是很快的。

    第二幅圖中:刪除了索引後的執行計劃,沒有使用索引,檢查的行數是81697,而我們只需要一行數據;而如果數據量不斷增加,再與其它表關聯查詢的話,其性能可想而知是有多低效。

    所以,查看是否檢查了過多的行,使用一些優化手段如利用好索引或者重構查詢儘量去減少檢查的行數。

  

  

  > 再看下面這個執行計劃:

      這個查詢聯接了多張表,僅第一張表就檢查了10W行(而我們只需要15行),然後再與其它表進行聯接,再排序,效率自然低下了。而其它檢查出只有一行的表,可看出其使用了索引列進行聯接,可見使用好索引的高效。

    看第二幅圖:使用了一個子查詢以減少檢查的行數,加上id列本身是排好序的,所以Extra列可以看到沒有使用臨時表進行文件排序了,在第一幅圖中,使用臨時表排序(using temporyary,using filesort)是很耗時的。

  

  

 

回到頂部

二、重構查詢

有些時候我們需要重寫查詢以獲取更好的性能,儘管得到的結果可能不同,也許最終程序的代碼也會和查詢一起被改。

1、是否可以把一個耗時的複雜查詢分解成多個簡單的查詢。

  > 平時我們更倡導用儘可能少的查詢做儘可能多的事情,這樣可以減少網絡通信開銷,能減少查詢解析和優化的步驟,以及代碼上似乎更優雅。

     但是在MySql中,MySql被設計成可以很高效地連接和斷開服務器,而且能很快地響應精簡的查詢。在現代網絡下,MySql在一般的服務器上每秒鐘可以處理50000個查詢。因此,對於一些耗時的複雜查詢,可以通過分解查詢以得到更高的效率。

2、分解聯接,把一個多表聯接分解成多個單表查詢,然後在應用程序端實現聯接

  > 例如有如下的一個連接查詢:

    SELECT * FROM tag JOIN tag_post ON tag.id = tag_post.tag_id WHERE tag.title = 'test';

   分解成兩個查詢:

    SELECT * FROM tag WHERE tag.title = 'test'; -- 假設返回id有 (10,11,12,13,14,15);

    SELECT * FROM tag_post WHERE tag_id IN (10,11,12,13,14,15);

   這樣分解查詢,看似浪費,但其針對一些耗時的多表聯接能帶來很好的性能提升:

    》 緩存的性能更高:上面的查詢已經被緩存起來,下次再查詢tag.title = 'test',則會直接從緩存中取出;第二條IN操作,下次查詢(11,12,14, 20,25),對於11,12,14則直接從緩存中取出,只去讀取20,25。如果一個表經常改變,分解聯接可以減少緩存失效的次數。

    》 可以減少多餘的行訪問,聯接操作,每從tag表中檢查一行,就會去tag_post中去檢查。

   > 什麼時候使用分解聯接更好: 可以緩存早期查詢的大量數據 , 數據分佈在不同的服務器上 , 對於大表使用IN()替換聯接

 

回到頂部

三、MySql如何優化和執行查詢

 下面這幅圖顯示了查詢的執行路徑:

   ① 客戶端將查詢發送到服務器;

   ② 服務器檢查查詢緩存,如果找到了,就從緩存中返回結果,否則進行下一步。

   ③ 服務器解析,預處理和優化查詢,生成執行計劃。

   ④ 執行引擎調用存儲引擎API執行查詢。

   ⑤ 服務器將結果發送回客戶端。

  

1、客戶端將查詢發送到服務器

  >首先需要知道,客戶端用一個數據包將查詢發送到服務器,一旦客戶端發送了查詢,剩下的就是等待結果。如果一個查詢過大,比如批量插入,有時會出現"MySQL server has gone away"的錯誤,導致的原因可能就是傳送的數據太大,導致連接斷開了,可以通過 SHOW VARIABLES LIKE "max_allowed_packet"  命令查看你的服務器所允許傳送的最大數據,可在my.ini裏配置。  

  > 服務器發送的響應由許多數據包組成,服務器發送響應的時候客戶端必須接收完整的結果集,不能只提取幾行數據後要求服務器停止發送剩下的數據。所以,使用LIMIT來獲取你所需要的數據行數。  

  > 每個MySql連接,或者叫線程,在任意一個給定的時間都有一個狀態來標識正在進行的事情。可以使用 SHOW [FULL] PROCESSLIST 命令來查看哪些線程正在運行,及其查詢狀態,Command列顯示了狀態。

   一些常見的狀態:其它的查找MySql手冊

    Sleep  線程正在等待客戶端,以向它發送一個新語句

    Query  線程正在執行查詢或往客戶端發送數據

    Locked  該查詢被其它查詢鎖定

    Copying to tmp table on disk  臨時結果集合大於tmp_table_size。線程把臨時表從存儲器內部格式改變爲磁盤模式,以節約存儲器

    Sending data  線程正在爲SELECT語句處理行,同時正在向客戶端發送數據

    Sorting for group  線程正在進行分類,以滿足GROUP BY要求

    Sorting for order  線程正在進行分類,以滿足ORDER BY要求  

  

2、服務器檢查查詢緩存

  > 在解析一個查詢之前,如果開啓了緩存,MySql會檢查查詢緩存,進行大小寫敏感的哈希查找。即使查詢和緩存中的查詢只有一個字節的差異,也表示不匹配,查詢就會進入下一步。

  > MySql查詢緩存保留了查詢返回給客戶端的完整結果,當緩存命中的時候,服務器馬上返回保存的結果(會先檢查權限),並跳過解析、優化和執行步驟。查詢緩存保留了查詢使用過的表,如果表發生了改變(如update),那麼緩存的數據就失效了。

3、服務器解析、優化,生成執行計劃

  > 如果查詢緩存中沒有,下一步就是將查詢轉變成執行計劃,包括解析、預處理和優化的過程。這個過程的任何一步都有可能出現錯誤,比如語法錯誤等。這裏我們可以看到平時出現的大部分錯誤是從哪一步拋出來的。

  > 首先是解析器將查詢分解成一個個標識,然後構造一顆“解析樹”,解析器保證查詢中的標識都是有效的,會檢查其中的基本錯誤,比如字符串上面的引號沒有閉合等。

  > 然後預處理器檢查解析器生成的解析樹,解決解析器無法解析的語義。比如,它會檢查表和列名是否存在,檢查名字和別名,保證沒有歧義。最後,預處理器檢查權限。

  > 之後,優化器把解析樹變成執行計劃。一個查詢通常可以有很多種執行方式,並且返回同樣的結果,優化器的任務就是找到最好的方式。

    》 MySql使用的是基於開銷的優化器。它會預測不同的執行計劃的開銷,並且選擇開銷最小的一個。可以使用 SHOW STATUS LIKE "Last_query_cost" 命令查看查詢的開銷(但不能作爲絕對標準)。如下表名最近一個查詢會造成29728次隨機讀取。

      

    》 但是優化器並不總是能選擇最好的方案。比如統計數據可能是錯誤的,服務器依賴於存儲引擎提供的統計,它可能很準確,可能很不準確。再比如優化器不會估算每一個可能的執行計劃,所以它可能會錯過優化方案。   

    》 MySql執行計劃是樹形結構,目的是指導執行引擎產生結果。最終的計劃中包含了足夠的信息來重建查詢。可以對某個查詢使用EXPLAIN EXTENDED 命令,並在結尾加上 SHOW WARNINGS,就可以看到重建後的查詢。

      下圖中,結果1顯示了查詢執行計劃,結果2中Message顯示了MySql優化後的查詢語句,也是最終執行的語句,可以複製出來看看。

      

    》 在後面可以看到MySql處理的一些優化類型。

4、查詢執行引擎執行查詢

  > MySql查詢執行引擎使用執行計劃來處理查詢。和優化部分相反,執行部分不會很複雜。MySql按照執行計劃的指令進行查詢(執行計劃時一個數據結構)。計劃中的許多操作都是通過存儲引擎提供的方法來完成的。 

5、返回結果到客戶端

  > 執行計劃的最後一步是將結果發送到客戶端,即使查詢沒有結果要返回,服務器也會對客戶端的聯接進行應答,比如有多少行受了影響。

  > 如果查詢是可緩存的,MySql會在這時緩存查詢。

  > 根據MySql的執行機制,一旦它處理了最後一個表並且成功地產生了一行輸出,他就會把這個結果發送到客戶端。這樣的好處是,服務器不用把這一行保存在內存中,二是服務端可以儘快的開始工作。

 

回到頂部

四、MySql能處理的一些優化類型

  MySql的優化器是相當複雜的,它使用了很多優化技巧把查詢轉換爲執行計劃。下面列出了MySql能處理的一些優化類型,以便我們去了解MySql優化器能夠做的工作。
但是,“不要試着比優化器更聰明”,不要想着去做一些優化器做的事情,有可能只會讓自己的查詢變得更復雜,更難以維護,除非你確實明白那樣做所帶來的影響。通常,應該讓優化器按照自己的方式來優化查詢。
你也可以通過EXPLAIN EXTENDED SELECT ... ... ; SHOW WARNINGS; 查看最終優化後的執行sql。

 

1、對聯接中的表重新排序

  > MySql優化器中最重要的部分是聯接優化器,它決定了多表查詢的最佳執行順序。通常可以用不同的順序聯接幾個表,然後得到同樣的結果。聯接優化器評估不同執行計劃的開銷,並且選擇開銷最低的計劃。後面再對聯接查詢細講。

2、 將外連接轉換成內連接 

  > 外聯接並不總是按照外聯接的方式進行,優化器有時能夠將外連接轉換爲等價的內聯接,以便適應其它的優化,比如排序

3、 代數等價法則

  > MySql使用代數化轉換來簡化並且規範化表達式。它可以隱藏或減少變量,移除不可能的限制和常量條件。

  > 比如,爲了方便,我們可能經常會這樣幹:WHERE 1=1 ,可以看到優化後的結果,1=1是被移除了的。  再比如,(a<b AND a=100) 被轉換爲 (b>100 AND a=100);條件如果是數字,直接比較數字的效率是最高的。

4、 優化COUNT()、MIN()、MAX()

  > 查找某列最大/最小值,該列又有索引,查找最大值,則會直接找最後一行;最小值,則直接找第一行。因爲索引已經排好序了。可以從EXPLAIN中看到:“選擇被優化掉的表”

  

  > COUNT(),對於MyIsam引擎總是保留着錶行數的精確值,查找所有行則直接取出,速度很快。

  > 但是,如果MIN()/MAX()查詢的列上沒有索引,則會進行全表掃描。

5、如果一個表達式可以被簡化爲一個常量,那麼這個表達式就會被轉換。 在WHERE 、USING、ON這些連接條件強制值相等的條件中,常量具有傳遞性

    > 可以看到等值聯接被轉換爲了兩個常量表達式。但這並不需要我們去手動寫成兩個常量表達式,優化器自會去做這些事情。

  

6、覆蓋索引

  > 先簡單說下覆蓋索引,以後再詳細討論索引的細節:覆蓋索引簡單的說就是索引上包含了該列的數據,如果是組合索引的話,就包含多列。比如有一個覆蓋索引:index_idCard_name(id_card, name),如果只查詢id_card和name的話,則可以通過id_card快速定位到該索引,並從索引中取出這兩列數據,從而避免了從磁盤中讀取該行數據。如果你還另外讀取了其它列,也會去讀取該行(這就涉及到隨機I/O的開銷了)。

  > 當索引包含查詢需要的列時,MySql就可以使用索引來避免讀取行數據。

  > 對於索引,優化器如果使用了一些複雜的算法來處理複雜的查詢語句,有可能會使用大量的CPU和內存資源,MySql並不會去考慮這些開銷,它只管如何高效地讀取數據;這時候查詢看上去開銷較低,實際上比整表掃描還慢。

  > 如果因爲優化器的限制而運行得很慢,可以通過IGNORE INDEX命令禁止一些索引。

7、 子查詢優化 

  > MySql可以將某些類型的子查詢轉換成相等的效率更高的形式,把它們簡化爲索引查找,而不是獨立的多個查詢。後面再詳細討論子查詢優化。

  > 但是,MySql有時把子查詢優化得很差。可以看到一個本來想要的IN列表被優化成了一個關聯查詢。類似於這種,可以自己手動改寫成JOIN關聯的方式。

  

8、 早期終結

  > 一旦滿足查詢或某個步驟的條件,MySql就會立即停止處理該查詢,或者該步驟。比如LIMIT子句,只要滿足LIMIT的數目,就會停止查詢。

  > 再比如,檢查到一個不可能的條件,他就會停止整個查詢,這個查詢在優化階段就停止了。

   

9、比較IN()裏面的數據

  > MySql會對IN()裏面的數據進行排序,然後用二分法查找某個值是否在列表中,這個算法的效率是O(Log n)。

  > 也許你會想到用OR代替IN(),等同的OR子句的查找效率是O(n),在列表很大的時候,OR子句會慢很多。

10、 MySql不會讓你在對同一個表進行UPDATE的同時運行SELECT。

  > 比如下面的更新,同時查詢,會報錯誤:You can't specify target table 'xp_user' for update in FROM clause; 你需要知道有這麼一個限制。

  

  > 但是,你可以以一種變通的方式來執行,比如使用關聯。

  

 

回到頂部

五、聯接查詢

1、MySql的聯接執行策略

  MySql的聯接執行策略很簡單,它把每個聯接都看成一個嵌套循環,這意味着MySql用一個循環從表中讀取數據,然後再用一個嵌套循環從下一個表中發現匹配數據。它不停地持續這個過程,當發現一行匹配的數據時,再根據SELECT子句中的列輸出;接着,查找下一個匹配的行。

  > 比如下面這個聯接查詢:

    SELECT  S.id, S.name, S.phone, C.name FROM student S, clazz C WHERE S.clazzid = C.id AND S.id < 5;

    MySql會先從student表中建立一個循環,查出一個滿足id<5的行,

      然後去clazz表建一個循環,查找滿足S.clazzid=C.id的行,

    找到一行後,就輸出SELECT的列,然後繼續查找下一行,

      遍歷完了之後,再回到student的循環,繼續查找,重複上面的步驟,直至全部查找出來。

  > 再看下面的這個左連接查詢:

    SELECT  S.id, S.name, S.phone, C.name FROM student S LEFT JOIN clazz C ON S.clazzid = C.id WHERE S.id < 5;

    同樣,先在student表中建立一個循環,查找一個滿足id<5的行,

      然後去clazz表建一個循環,查找滿足S.clazzid=C.id的行,

    如果找到了滿足的行,則輸出SELECT的列;如果沒找到,則輸出student相關SELECT的列,而clazz表的列則輸出NULL,這就是左連接

    遍歷完後,再回到student的循環,繼續查找,重複步驟,直至全部查找出來。

  > 從本質上來說,MySql以同樣的方式執行每一種查詢。例如,在處理FROM子句中的查詢時,它會先執行子查詢,並且把結果放到臨時表裏面,然後把臨時表當成普通表進行下一步處理,因而它叫衍生表(Derived Table)。看圖一

  > MySql也使用臨時表來處理聯合(UNION),MySql將UNION看成一系列的單個查詢,它們將結果寫入臨時表中,最後再讀取出來組成最終結果。看圖二

  > MySql會把所有的右聯接(RIGHT [OUTER] JOIN)改寫成等價的左連接(LEFT [OUTER] JOIN),可以通過SHOW WARNINGS查看最終的執行語句。看圖三

     在MySql中,每個單個查詢都是一個聯接,所以從臨時表讀取數據實際也是聯接。

     順便一提:臨時表沒有索引,只是存放數據,所以原表的索引,約束等都是不起作用的。

  >正是因爲MySql的這種聯接策略,所以MySql不支持全外聯接(FULL OUTER JOIN),全外聯接不能用嵌套循環,因爲在檢索第一個表時可能就沒有匹配的數據。

     圖一:

    

    圖二:

     

     圖三:

    

2、聯接優化器

  > MySql優化器中最重要的部分是聯接優化器,它決定了多表查詢的最佳執行順序,這樣一般可以減少讀取的行數。通常可以用不同的順序聯接幾個表,然後得到同樣的結果。聯接優化器評估不同的執行計劃的開銷,並且選擇開銷最低的計劃。

  > 對聯接重新排序通常是一種非常有效的優化手段。但重新排序有時並不是最佳的執行計劃,這是可以使用STRAIGHT_JOIN參數,並且按照你認爲最佳的方式來組織聯接的順序;但是這種情況是很少見的,聯接優化器比人更能精確的計算開銷。

 

回到頂部

六、優化特定類型的查詢

1、優化COUNT

  > 通常來說,使用了COUNT的查詢很難優化,因爲他們需要統計很多行(訪問很多數據)。在MySql內部優化它的唯一其他選擇就是使用覆蓋索引。還不夠,就需要考慮更改應用程序的架構。可以考慮使用匯總表,還可以利用外部緩存系統。

  > COUNT有兩個不同的工作方式:統計值的數量和統計行的數量。值是一個非空的表達式(NULL意味着沒有值)。

    如果COUNT()的括號中定義了列名或其它表達式,COUNT就會統計這個表達式有值的次數。

    COUNT的另一種形式就是統計結果中行的數量。當MySql知道括號中的表達式永遠都不會爲NULL的時候,它就會按這種方式工作。例如COUNT(*),它是COUNT的一種特例,它不會把通配符*展開成所有的列,而是忽略所有的列並統計行數。

  > 關於COUNT(NULL)的應用:當你統計某列不同值的數量時,可以像下面這樣寫SQL

    1.使用SUM()函數:SELECT SUM(IF(c1='red', 1, 0)) AS red, SUM(IF(c1='blue'), 1, 0) AS blue, SUM(IF(c1='black'), 1, 0) AS black FROM color;

    2.使用COUNT():SELECT COUNT(c1='red' OR NULL) AS red, COUNT(c1='blue' OR NULL) AS blue, COUNT(c1='black' OR NULL) AS black FROM color;

2、優化聯接

  > 確保ON或USING使用的列上有索引。

  > 確保GROUP BY或ORDER BY只引用了一個表中的列,這樣,MySql可以嘗試對這些操作使用索引。

3、優化GROUP BY和DISTINCT

  > 在很多情況下,MySql對這兩種方式的優化方式基本都是一樣的。實際上,優化過程要求他們可以互相轉化。通常來說,索引是優化它們的一種重要的手段。

  > 當不能使用索引的時候,MySql有兩種優化GROUP BY的策略:使用臨時表或文件排序進行分組。任何一種方式對於特定的查詢都有可能是高效的。可以使用SQL_SMALL_RESULT強制MySql選擇臨時表,或者使用SQL_BIG_RESULT強制它使用文件排序。

4、優化LIMIT和OFFSET

  > 一個常見的問題是偏移量很大,比如查詢使用了LIMIT 10000, 20,它就會產生10020行數據,並且丟掉前10000行。這個操作的代價非常高。

  > 一個提高效率的簡單技巧就是在覆蓋索引上進行偏移,而不是對全行數據進行偏移。可以將從覆蓋索引上提取出來的數據和全行數據進行聯接,然後取得需要的列。這會更有效率。

  > 一個較好的設計是把頁面調度放到"下一頁"鏈接上,假設每頁只顯示20個結果,那麼查詢就應該LIMIT 21行數據,但是隻顯示20行,如果結果中有第21行,則有下一頁。

  > 可以提取並緩存大量數據,比如1000行數據,然後從緩存中獲取後續頁面的數據。

 

回到頂部

七、查詢優化提示

如果不滿意MySql優化器選擇的優化方案,可以使用一些優化提示來控制優化器的行爲。可以將適當的提示放入查詢中,它只會影響當前的查詢。

1、DELAYED:這個提示用於INSERT和UPDATE。

  應用了這個提示的語句會立即返回並將待插入的列放入緩衝區中,在表空閒的時候再執行插入。它對於記錄日誌很有用,對於某些需要插入大量數據也很有用。它有很多限制,比如,延遲插入不能運行於所有的存儲引擎上,並且無法使用LAST_INSERT_ID();

2、STRAIGHT_JOIN:

  這個提示可用於SELECT語句中SELECT關鍵字後面,也可以用於聯接語句。它的一個用途是強制MySql按照查詢中表出現的順序來聯接表;另一個用途是聯接兩個表時,強制這兩個表按照順序聯接。

3、SQL_SMALL_RESULT和SQL_BIG_RESULT

  用於SELECT語句。它們告訴MySql在GROUP BY或DISTINCT查詢中如何並且何時使用臨時表。SQL_SMALL_RESULT告訴優化器結果集會比較小,可以放在索引過的臨時表中,以避免對分組後的數據排序。SQL_BIG_RESULT表明結果集比較大,最好使用磁盤上的臨時表排序。

4、SQL_BUFFER_RESULT

  這個提示告訴優化器將結果放在臨時表中,並且儘快釋放掉表鎖。

5、SQL_CACHE和SQL_NO_CACHE

  SQL_CACHE表明將查詢緩存;SQL_NO_CACHE則相反。

6、USING INDEX、IGNORE INDEX和FORCE INDEX

  這幾個提示告訴優化器從表中尋找行的時候使用或忽略索引。

 

好了 至此也算是簡單的深入瞭解了MySql內部的運行機制,相信這對於以後學習高效運用MySql是非常有幫助的。

 

作者:bojiangzhou

出處:http://www.cnblogs.com/chiangchou/

本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章