1. 程式人生 > >MySQL查詢過程剖析

MySQL查詢過程剖析

  資料庫的效能問題是我們一直需要考慮的一個問題。有人可能說建立最好的索引,設計最有的庫表結構。但是這些在沒有合理的設計查詢的基礎上,都無法實現高效能。
  今天,我們就先聊一聊MySQL的查詢過程。下圖就是MySQL的查詢過程圖:

MySQL的查詢過程

這裡寫圖片描述
   首先,對其中的各個模組做一簡單分析:
  

1、客戶端/服務端通訊協議

  首先,需要說明的是:MySQL客戶端/服務端通訊協議是“半雙工”的:在任一時刻,要麼是伺服器向客戶端傳送資料,要麼是客戶端向伺服器傳送資料,這兩個動作不能同時發生。
  
  一旦一端開始傳送訊息,另一端要接收完整個訊息才能響應它,所以我們無法也無須將一個訊息切成小塊獨立傳送,也沒有辦法進行流量控制。
  
  客戶端用一個單獨的資料包將查詢請求傳送給伺服器,所以當查詢語句很長的時候,需要設定max_allowed_packet引數。因為MySQL根據配置檔案會限制Server接受的資料包大小。有時候大的插入和更新會受 max_allowed_packet 引數限制,導致寫入或者更新失敗。但是需要注意的是,如果查詢實在是太大,服務端會拒絕接收更多資料並丟擲異常。

show VARIABLES like '%max_allowed_packet%';  --檢視max_allowed_packet的大小
修改max_allowed_packet 引數:
1、修改mysql配置檔案:
2、mysql命令列中修改:set global max_allowed_packet = 20*1024*1024

  與之相反的是,伺服器響應給使用者的資料通常會很多,由多個數據包組成。但是當伺服器響應客戶端請求時,客戶端必須完整的接收整個返回結果,而不能簡單的只取前面幾條結果,然後讓伺服器停止傳送。因而在實際開發中,儘量保持查詢簡單且只返回必需的資料,減小通訊間資料包的大小和數量是一個非常好的習慣,這也是查詢中儘量避免使用SELECT *以及加上LIMIT限制的原因之一。

2、查詢快取

  在解析一個查詢語句前,如果查詢快取是開啟的,那麼MySQL會檢查這個查詢語句是否命中查詢快取中的資料。如果當前查詢恰好命中查詢快取,在檢查一次使用者許可權後直接返回快取中的結果。這種情況下,查詢不會被解析,也不會生成執行計劃,更不會執行,從而提高查詢效率。

  需要說明的是:MySQL將快取存放在一個引用表(類似於HashMap的資料結構),通過一個雜湊值索引,這個雜湊值通過查詢本身、當前要查詢的資料庫、客戶端協議版本號等一些可能影響結果的資訊計算得來。因為是依靠hash值查詢 ,所以命中快取就需要兩者嚴格一致。兩個查詢在任何字元上的不同(例如:空格、註釋),都會導致快取不命中。

不會使用查詢快取的情況
  如果查詢中包含任何使用者自定義函式、儲存函式、使用者變數、臨時表、MySQL庫中的系統表,其查詢結果都不會被快取。

// 不會使用查詢快取
$r = mysql_query("SELECT username FROM user WHERE signup_date >= CURDATE()");

// 使用查詢快取
$today = date("Y-m-d");
$r = mysql_query("SELECT username FROM user WHERE signup_date >= '$today'");

  上面兩條SQL語句的差別就是 CURDATE() ,MySQL的查詢快取對這個函式不起作用。所以,像 NOW() 和 RAND() 或是其它的諸如此類的SQL函式都不會開啟查詢快取,因為這些函式的返回是會不定的易變的。所以,你所需要的就是用一個變數來代替MySQL的函式,從而開啟快取。

導致快取失效的情況

  既然是快取,就會失效,那查詢快取何時失效呢?MySQL的查詢快取系統會跟蹤查詢中涉及的每個表,如果這些表(資料或結構)發生變化,那麼和這張表相關的所有快取資料都將失效。正因為如此,在任何的寫操作時,MySQL必須將對應表的所有快取都設定為失效。如果查詢快取非常大或者碎片很多,這個操作就可能帶來很大的系統消耗,甚至導致系統僵死一會兒。而且查詢快取對系統的額外消耗也不僅僅在寫操作,讀操作也不例外:

1、任何的查詢語句在開始之前都必須經過檢查,即使這條SQL語句永遠不會命中快取
2、如果查詢結果可以被快取,那麼執行完成後,會將結果存入快取,也會帶來額外的系統消耗

  所以,我們需要知道的是:並不是什麼情況下查詢快取都會提高系統性能,快取和失效都會帶來額外消耗,只有當快取帶來的資源節約大於其本身消耗的資源時,才會給系統帶來效能提升。但要如何評估開啟快取是否能夠帶來效能提升是一件非常困難的事情,也不在本文討論的範疇內。如果系統確實存在一些效能問題,可以嘗試開啟查詢快取,並在資料庫設計上做一些優化,比如:

1、用多個小表代替一個大表,注意不要過度設計
2、批量插入代替迴圈單條插入
3、合理控制快取空間大小,一般來說其大小設定為幾十兆比較合適
4、可以通過SQL_CACHE和SQL_NO_CACHE來控制某個查詢語句是否需要進行快取

注意:
  不要輕易開啟查詢快取,特別是寫密集型應用。如果實在需要的話,可以將query_cache_type設定為DEMAND,這時只有加入SQL_CACHE的查詢才會走快取,其他查詢則不會,這樣可以非常自由地控制哪些查詢需要被快取。

3、語法解析和預處理

  MySQL通過關鍵字將SQL語句進行解析,並生成一棵對應的“解析樹”。MySQL解析器將使用MySQL語法規則驗證和解析查詢。例如,它將驗證是否使用錯誤的關鍵字,或者使用關鍵字的順序是否正確等,再或者它還會驗證引號是否能前後正確匹配。
  
  預處理則根據一些MySQL規則進一步檢查解析樹是否合法,例如,這裡將檢查資料表和資料列是否存在,還會解析名字和別名,看看它們是否有歧義。
  下一步前處理器會驗證許可權。這通常很快,除非伺服器上有非常多的許可權配置。

查詢優化

  上述說到,MySQL會解析查詢,並建立內部資料結構(解析樹)。接下來會對其進行各種優化。包括重寫查詢、決定表的讀取順序,以及選擇合適的索引等。

  MySQL使用基於成本的優化器,它嘗試預測一個查詢使用某種執行計劃時的成本,並選擇其中成本最小的一個(注意;成本小,並不代表執行速度快)。在MySQL可以通過查詢當前會話的last_query_cost的值來得到其計算當前查詢的成本。

select * from User;
...
show status like 'last_query_cost';
+-----------------+----------+
| Variable_name   | Value    |
+-----------------+----------+
| Last_query_cost | 7.999000 |
+-----------------+----------+

  上述示例中的結果表示優化器認為大概需要做7個數據頁的隨機查詢才能完成上面的查詢。這個結果是根據一些列的統計資訊計算得來的,這些統計資訊包括:每張表或者索引的頁面個數、索引的基數、索引和資料行的長度、索引的分佈情況等等。
  
  使用者可以通過特殊的關鍵字提示(hint)優化器,影響它的決策過程。也可以通過請求優化器解釋(explain)優化過程的各個因素,是使用者可以知道伺服器時如何進行優化決策的,並提供一個參考基準,便於使用者重構查詢和schema、修改相關的配置,使應用盡可能的高效執行。

mysql> EXPLAIN SELECT * FROM user;
+----+-------------+----------+------+---------------+------+---------+------+--------+-------+
| id | select_type | table    | type | possible_keys | key  | key_len | ref  | rows   | Extra |
+----+-------------+----------+------+---------------+------+---------+------+--------+-------+
|  1 | SIMPLE      | customer | ALL  | NULL          | NULL | NULL    | NULL | 161    |       |
+----+-------------+----------+------+---------------+------+---------+------+--------+-------+

  MySQL的查詢優化器是一個非常複雜的部件,它使用了非常多的優化策略來生成一個最優的執行計劃,下面是一些常用的優化策略:
  1、重新定義表的關聯順序(多張表關聯查詢時,並不一定按照SQL中指定的順序進行,但有一些技巧可以指定關聯順序);
  2、優化MIN()和MAX()函式(找某列的最小值,如果該列有索引,只需要查詢B+Tree索引最左端,反之則可以找到最大值);
  3、提前終止查詢(比如:使用Limit時,查詢到滿足數量的結果集後會立即終止查詢)

 需要說明的是,優化器並不關心表使用的是什麼儲存引擎。但是,儲存引擎對於優化查詢是有影響的。優化器會請求儲存引擎提供容量或某個具體操作的開銷資訊,以及表資料的統計資訊等。例如:某些儲存引擎的某種索引,可能對一些特定的查詢有優化。

  對於SELECT語句,在解析查詢之前,伺服器會先檢查Query Cache(查詢快取),如果可以在其中找到對應的查詢,伺服器就不必在執行查詢解析、優化和執行的整個過程,而是直接返回查詢快取中的結果集。

4、查詢執行引擎

  在完成解析和優化階段以後,MySQL會生成對應的執行計劃,查詢執行引擎根據執行計劃給出的指令逐步執行得出結果。整個執行過程的大部分操作均是通過呼叫儲存引擎實現的介面來完成,這些介面被稱為handler API。查詢過程中的每一張表由一個handler例項表示。
  實際上,MySQL在查詢優化階段就為每一張表建立了一個handler例項,優化器可以根據這些例項的介面來獲取表的相關資訊,包括表的所有列名、索引統計資訊等。儲存引擎介面提供了非常豐富的功能,但其底層僅有幾十個介面,這些介面像搭積木一樣完成了一次查詢的大部分操作。

5、返回結果給客戶端

  查詢執行的最後一個階段就是將結果返回給客戶端。即使查詢不到資料,MySQL仍然會返回這個查詢的相關資訊,比如該查詢影響到的行數以及執行時間等。

  如果查詢快取被開啟且這個查詢可以被快取,MySQL也會將結果存放到快取中。

  結果集返回客戶端是一個增量且逐步返回的過程。有可能MySQL在生成第一條結果時,就開始向客戶端逐步返回結果集了。這樣服務端就無須儲存太多結果而消耗過多記憶體,也可以讓客戶端第一時間獲得返回結果。需要注意的是,結果集中的每一行都會以一個滿足通訊協議的資料包傳送,再通過TCP協議進行傳輸,在傳輸過程中,可能對MySQL的資料包進行快取然後批量傳送。

總結

MySQL整個查詢執行過程:
1、客戶端同資料庫服務層建立TCP連線。
2、客戶端向MySQL伺服器傳送一條查詢請求。
3、連線執行緒接收到SQL語句之後,將語句交給SQL語句解析模組進行語法分析和語義分析。
4、先看查詢快取中是否有結果,如果有結果可以直接返回給客戶端。
5、如果查詢快取中沒有結果,就需要真的查詢資料庫引擎層了,於是發給SQL優化器,進行查詢的優化,生成相應的執行計劃。
6、MySQL根據執行計劃,呼叫儲存引擎的API來執行查詢
7、使用儲存引擎查詢時,先開啟表,如果需要的話獲取相應的鎖。 查詢快取頁中有沒有相應的資料,如果有則可以直接返回,如果沒有就要從磁碟上去讀取。
8、當在磁碟中找到相應的資料之後,則會載入到快取中來,從而使得後面的查詢更加高效,由於記憶體有限,多采用變通的LRU表來管理快取頁,保證快取的都是經常訪問的資料。
9、最後,獲取資料後返回給客戶端,關閉連線,釋放連線執行緒。