MYSQL優化(二):查詢優化
語法解析和預處理
MySQL通過關鍵字將SQL語句進行解析,並生成一顆對應的解析樹。這個過程解析器主要通過語法規則來驗證和解析。比如SQL中是否使用了錯誤的關鍵字或者關鍵字的順序是否正確等等。預處理則會根據MySQL規則進一步檢查解析樹是否合法。比如檢查要查詢的資料表和資料列是否存在等等。
查詢優化
經過前面的步驟生成的語法樹被認為是合法的了,並且由優化器將其轉化成查詢計劃。多數情況下,一條查詢可以有很多種執行方式,最後都返回相應的結果。優化器的作用就是找到這其中最好的執行計劃。
MySQL使用基於成本的優化器,它嘗試預測一個查詢使用某種執行計劃時的成本,並選擇其中成本最小的一個。在MySQL可以通過查詢當前會話的
last_query_cost
的值來得到其計算當前查詢的成本。示例中的結果表示優化器認為大概需要做6391個數據頁的隨機查詢才能完成上面的查詢。這個結果是根據一些列的統計資訊計算得來的,這些統計資訊包括:每張表或者索引的頁面個數、索引的基數、索引和資料行的長度、索引的分佈情況等等。
有非常多的原因會導致MySQL選擇錯誤的執行計劃,比如統計資訊不準確、不會考慮不受其控制的操作成本(使用者自定義函式、儲存過程)、MySQL認為的最優跟我們想的不一樣(我們希望執行時間儘可能短,但MySQL值選擇它認為成本小的,但成本小並不意味著執行時間短)等等。
MySQL的查詢優化器是一個非常複雜的部件,它使用了非常多的優化策略來生成一個最優的執行計劃:
重新定義表的關聯順序(多張表關聯查詢時,並不一定按照SQL中指定的順序進行,但有一些技巧可以指定關聯順序)
優化
MIN()
和MAX()
函式(找某列的最小值,如果該列有索引,只需要查詢B+Tree索引最左端,反之則可以找到最大值,具體原理見下文)提前終止查詢(比如:使用Limit時,查詢到滿足數量的結果集後會立即終止查詢)
優化排序(在老版本MySQL會使用兩次傳輸排序,即先讀取行指標和需要排序的欄位在記憶體中對其排序,然後再根據排序結果去讀取資料行,而新版本採用的是單次傳輸排序,也就是一次讀取所有的資料行,然後根據給定的列排序。對於I/O密集型應用,效率會高很多)
隨著MySQL的不斷髮展,優化器使用的優化策略也在不斷的進化,這裡僅僅介紹幾個非常常用且容易理解的優化策略,其他的優化策略,大家自行查閱吧。
查詢執行引擎
在完成解析和優化階段以後,MySQL會生成對應的執行計劃,查詢執行引擎根據執行計劃給出的指令逐步執行得出結果。整個執行過程的大部分操作均是通過呼叫儲存引擎實現的介面來完成,這些介面被稱為handler API
。查詢過程中的每一張表由一個handler
例項表示。實際上,MySQL在查詢優化階段就為每一張表建立了一個handler
例項,優化器可以根據這些例項的介面來獲取表的相關資訊,包括表的所有列名、索引統計資訊等。儲存引擎介面提供了非常豐富的功能,但其底層僅有幾十個介面,這些介面像搭積木一樣完成了一次查詢的大部分操作。
返回結果給客戶端
查詢執行的最後一個階段就是將結果返回給客戶端。即使查詢不到資料,MySQL仍然會返回這個查詢的相關資訊,比如該查詢影響到的行數以及執行時間等等。
如果查詢快取被開啟且這個查詢可以被快取,MySQL也會將結果存放到快取中。
結果集返回客戶端是一個增量且逐步返回的過程。有可能MySQL在生成第一條結果時,就開始向客戶端逐步返回結果集了。這樣服務端就無須儲存太多結果而消耗過多記憶體,也可以讓客戶端第一時間獲得返回結果。需要注意的是,結果集中的每一行都會以一個滿足①中所描述的通訊協議的資料包傳送,再通過TCP協議進行傳輸,在傳輸過程中,可能對MySQL的資料包進行快取然後批量傳送。
回頭總結一下MySQL整個查詢執行過程,總的來說分為6個步驟:
客戶端向MySQL伺服器傳送一條查詢請求
伺服器首先檢查查詢快取,如果命中快取,則立刻返回儲存在快取中的結果。否則進入下一階段
伺服器進行SQL解析、預處理、再由優化器生成對應的執行計劃
MySQL根據執行計劃,呼叫儲存引擎的API來執行查詢
將結果返回給客戶端,同時快取查詢結果
效能優化建議
看了這麼多,你可能會期待給出一些優化手段,是的,下面會從3個不同方面給出一些優化建議。但請等等,還有一句忠告要先送給你:不要聽信你看到的關於優化的“絕對真理”,包括本文所討論的內容,而應該是在實際的業務場景下通過測試來驗證你關於執行計劃以及響應時間的假設。
Scheme設計與資料型別優化
選擇資料型別只要遵循小而簡單的原則就好,越小的資料型別通常會更快,佔用更少的磁碟、記憶體,處理時需要的CPU週期也更少。越簡單的資料型別在計算時需要更少的CPU週期,比如,整型就比字元操作代價低,因而會使用整型來儲存ip地址,使用DATETIME
來儲存時間,而不是使用字串。
這裡總結幾個可能容易理解錯誤的技巧:
通常來說把可為
NULL
的列改為NOT NULL
不會對效能提升有多少幫助,只是如果計劃在列上建立索引,就應該將該列設定為NOT NULL
。對整數型別指定寬度,比如
INT(11)
,沒有任何卵用。INT
使用32位(4個位元組)儲存空間,那麼它的表示範圍已經確定,所以INT(1)
和INT(20)
對於儲存和計算是相同的。UNSIGNED
表示不允許負值,大致可以使正數的上限提高一倍。比如TINYINT
儲存範圍是-128 ~ 127,而UNSIGNED TINYINT
儲存的範圍卻是0 - 255。通常來講,沒有太大的必要使用
DECIMAL
資料型別。即使是在需要儲存財務資料時,仍然可以使用BIGINT
。比如需要精確到萬分之一,那麼可以將資料乘以一百萬然後使用BIGINT
儲存。這樣可以避免浮點數計算不準確和DECIMAL
精確計算代價高的問題。TIMESTAMP
使用4個位元組儲存空間,DATETIME
使用8個位元組儲存空間。因而,TIMESTAMP
只能表示1970 - 2038年,比DATETIME
表示的範圍小得多,而且TIMESTAMP
的值因時區不同而不同。大多數情況下沒有使用列舉型別的必要,其中一個缺點是列舉的字串列表是固定的,新增和刪除字串(列舉選項)必須使用
ALTER TABLE
(如果只只是在列表末尾追加元素,不需要重建表)。schema的列不要太多。原因是儲存引擎的API工作時需要在伺服器層和儲存引擎層之間通過行緩衝格式拷貝資料,然後在伺服器層將緩衝內容解碼成各個列,這個轉換過程的代價是非常高的。如果列太多而實際使用的列又很少的話,有可能會導致CPU佔用過高。
大表
ALTER TABLE
非常耗時,MySQL執行大部分修改表結果操作的方法是用新的結構建立一個張空表,從舊錶中查出所有的資料插入新表,然後再刪除舊錶。尤其當記憶體不足而表又很大,而且還有很大索引的情況下,耗時更久。當然有一些奇技淫巧可以解決這個問題,有興趣可自行查閱。