1. 程式人生 > 實用技巧 >MySQL效能優化原理 — 前篇

MySQL效能優化原理 — 前篇

前言

效能優化(Optimize)指的是在保證系統正確性的前提下,能夠更快速響應請求的一種手段。而且有些效能問題,比如慢查詢等,如果積累到一定的程度或者是遇到急速上升的併發請求之後,會導致嚴重的後果,輕則造成服務繁忙,重則導致應用不可用。它對我們來說就像一顆即將被引爆的定時炸彈一樣,時刻威脅著我們。因此在上線專案之前需要嚴格的把關,以確保 MySQL 能夠以最優的狀態進行執行。同時,MySQL 的效能優化無法一蹴而就,必須一步一步慢慢來,從各個方面進行優化,最終效能就會有大的提升。

對 MySQL 優化是一個綜合性的技術,主要包括

  • 表的設計合理化(符合3NF)
  • 新增適當索引(index) [四種: 普通索引、主鍵索引、唯一索引unique、全文索引]
  • 分表技術(水平分割、垂直分割)
  • 讀寫[寫: update/delete/add]分離
  • 儲存過程 [模組化程式設計,可以提高速度]
  • 對mysql配置優化 [配置最大併發數my.ini, 調整快取大小 ]
  • mysql伺服器硬體升級
  • 定時的去清除不需要的資料,定時進行碎片整理(MyISAM)

本篇主要從MySQL的邏輯結構和查詢過程進行分析,下一篇重點來梳理MySQL效能優化的幾個方面。對於MySQL索引的實現原理可以檢視這篇《深入理解MySQL索引底層資料結構與演算法》

MySQL邏輯結構

MySQL邏輯架構整體分為三層,最上層為客戶端層,並非MySQL所獨有,如:連線處理、授權認證、安全等功能均在這一層處理。

MySQL大多數核心服務均在中間這一層,包括查詢解析、分析、優化、快取、內建函式(比如:時間、數學、加密等函式)。所有的跨儲存引擎的功能也在這一層實現:儲存過程、觸發器、檢視等。

最下層為儲存引擎,其負責MySQL中的資料儲存和提取。和Linux下的檔案系統類似,每種儲存引擎都有其優勢和劣勢。中間的服務層通過API與儲存引擎通訊,這些API介面遮蔽了不同儲存引擎間的差異。

MySQL 查詢過程

為了使 MySQL 能夠獲取更高的查詢效能,我們需要弄清楚 MySQL 是如何優化和執行查詢的。一旦理解了這一點就會發現,很多查詢優化公眾實際上就是遵循一些原則讓 MySQL 的優化器能夠安裝預想的合理方式執行而已。

當客戶端向 MySQL 傳送一個請求的時候,MySQL到底做了些什麼呢?

客戶端/服務端通訊協議

MySQL 客戶端 / 服務端通訊協議是“半雙工”的,在任意時刻,要麼是伺服器向客戶端傳送資料,要麼是客戶端向伺服器傳送資料,這兩個動作不能同時發生。一旦一端開始傳送訊息,另一端需要接收完整訊息才能響應它,所以我們無法也無須將一個訊息切成小塊獨立傳送,也沒有辦法進行流量控制。

客戶端用一個單獨的資料包將查詢請求傳送給伺服器,所以當查詢語句很長的時候,需要設定max_allowed_packet引數。但是需要注意的是,如果查詢實在太大,服務端會拒絕接收更多資料並丟擲異常。

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

通訊方式分為:單工通訊、半雙工、全雙工。

  1. 全雙工的典型例子是:打電話。電話在接到聲音的同時也會傳遞聲音。在一個時刻,線路上允許兩個方向上的資料傳輸。網絡卡也是雙工模式。在接收資料(比如一直在下載東西)的同時,又傳送資料(比如請求網頁)
  2. 半雙工:在同一個時刻只能進行一個動作。在一個時刻,線路上只允許一個方向上的資料傳輸。
  3. 單工:典型例子就是電視遙控器。接收端和傳送端已經固定了的。接收端只能接收資料。沒有傳送資料的功能。傳送端只有傳送的功能。沒有接收的功能.特點是,線路上的資料流是永遠是單方向,固定方向。這樣理解的話,顯示器與電腦主機之間的工作方式也是單工模式的。

查詢快取

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

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

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

比如函式NOW()或者CURRENT_DATE()會因為不同的查詢時間,返回不同的查詢結果,再比如包含CURRENT_USER或者CONNECION_ID()的查詢語句會因為不同的使用者而返回不同的結果,將這樣的查詢結果快取起來沒有任何的意義。

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

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

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

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

最後的建議不要輕易開啟查詢快取,特別是寫密集型應用。如果你想試一下,可以將query_cache_type設定為DEMAND,這時只有加入SQL_CACHE的查詢才會走快取,其他查詢則不會,這也可以非常自由地控制哪些查詢需要被快取。

當然查詢快取系統本身是非常複雜的,這裡討論的也只是很小的一部分,其他更深入的話題,比如:快取是如何使用記憶體的?如何控制記憶體的碎片化?事務對查詢快取有何影響等等,可以自行閱讀相關資料,這裡權當拋磚引玉吧。

語法解析和預處理

MySQL 通過關鍵字將 SQL 語句進行解析,並生成一顆對應的解析樹。這個過程解析器主要通過語法規則來驗證和解析。比如 SQL 中是否使用了錯誤的關鍵字或者關鍵字的順序是否正確等待。預處理則會根據 MySQL 規則進一步檢查解析樹是否合法。比如檢查要查詢的資料表和資料列是否存在等等。

查詢優化

經過前面的步驟生成的語法樹被認為是合法的了,並且由優化器將其轉化成查詢計劃。多數情況下,一條查詢可以有很多種執行方式,最後都返回相應的結果。優化器的作用就是找到這其中最好的執行計劃。

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

mysql> select * from sys_log limit 10;  
...省略結果集  
  
mysql> show status like 'last_query_cost';  
+-----------------+-------------+  
| Variable_name   | Value       |  
+-----------------+-------------+  
| Last_query_cost | 6391.799000 |  
+-----------------+-------------+  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

示例中的結果表示優化器認為大概需要做6391個數據頁的隨機查詢才能完成上面的查詢。這個結果是根據一些列的統計資訊計算得來的,這些統計資訊包括:每張表或者索引的頁面個數、索引的基數、索引和資料行的長度、索引的分佈情況等等。

有非常多的原因會導致MySQL選擇錯誤的執行計劃,比如統計資訊不準確、不會考慮不受其控制的操作成本(使用者自定義函式、儲存過程)、MySQL認為的最優跟我們想的不一樣(我們希望執行時間儘可能短,但MySQL值選擇它認為成本小的,但成本小並不意味著執行時間短)等等。

MySQL的查詢優化器是一個非常複雜的部件,它使用了非常多的優化策略來生成一個最優的執行計劃:

  • 重新定義表的關聯順序(多張表關聯查詢時,並不一定按照SQL中指定的順序進行,但有一些技巧可以指定關聯順序)
  • 優化MIN()和MAX()函式(找某列的最小值,如果該列有索引,只需要查詢B+Tree索引最左端,反之則可以找到最大值,具體原理檢視《MySQL索引底層原理》
  • 提前終止查詢(比如:使用Limit時,查詢到滿足數量的結果集後會立即終止查詢)
  • 優化排序(在老版本MySQL會使用兩次傳輸排序,即先讀取行指標和需要排序的欄位在記憶體中對其排序,然後再根據排序結果去讀取資料行,而新版本採用的是單次傳輸排序,也就是一次讀取所有的資料行,然後根據給定的列排序。對於I/O密集型應用,效率會高很多)

隨著MySQL的不斷髮展,優化器使用的優化策略也在不斷的進化,這裡僅僅介紹幾個非常常用且容易理解的優化策略,其他的優化策略,大家自行查閱吧。

查詢執行引擎

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

返回結果給客戶端

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

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

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

總結

MySQL 整個查詢執行過程,總的來說分為6個步驟:

  • 客戶端向MySQL伺服器傳送一條查詢請求;
  • 伺服器首先檢查查詢快取,如果命中快取,則立刻返回儲存在快取中的結果。否則進入一下階段;
  • 伺服器進行SQL解析、預處理、再由優化器生成對應的執行計劃;
  • MySQL 根據執行計劃,呼叫儲存引擎的API來執行查詢;
  • 將結果返回給客戶端,同時快取查詢結果。