提及MySQL的查詢優化,相信你們收藏了一堆奇技淫巧:不能使用SELECT *、不使用NULL字段、合理建立索引、爲字段選擇合適的數據類型….. 你是否真的理解這些優化技巧?是否理解其背後的工做原理?在實際場景下性能真有提高嗎?我想未必。於是理解這些優化建議背後的原理就尤其重要,但願本文能讓你從新審視這些優化建議,並在實際業務場景下合理的運用。(因文章篇幅過長,將分爲兩篇進行解析)mysql
若是能在頭腦中構建一幅MySQL各組件之間如何協同工做的架構圖,有助於深刻理解MySQL服務器。下圖展現了MySQL的邏輯架構圖。面試
**客戶端層:**MySQL邏輯架構總體分爲三層,最上層爲客戶端層,並不是MySQL所獨有,諸如:鏈接處理、受權認證、安全等功能均在這一層處理。sql
**核心服務層:**MySQL大多數核心服務均在中間這一層,包括查詢解析、分析、優化、緩存、內置函數(好比:時間、數學、加密等函數)。全部的跨存儲引擎的功能也在這一層實現:存儲過程、觸發器、視圖等。數據庫
**存儲引擎層:**最下層爲存儲引擎,其負責MySQL中的數據存儲和提取。和Linux下的文件系統相似,每種存儲引擎都有其優點和劣勢。中間的服務層經過API與存儲引擎通訊,這些API接口屏蔽了不一樣存儲引擎間的差別。緩存
咱們老是但願MySQL可以得到更高的查詢性能,最好的辦法是弄清楚MySQL是如何優化和執行查詢的。一旦理解了這一點,就會發現:不少的查詢優化工做實際上就是遵循一些原則讓MySQL的優化器可以按照預想的合理方式運行而已。安全
當向MySQL發送一個請求的時候,MySQL到底作了些什麼呢?bash
客戶端/服務端通訊協議 MySQL客戶端/服務端通訊協議是「半雙工」的:在任一時刻,要麼是服務器向客戶 端發送數據,要麼是客戶端向服務器發送數據,這兩個動做不能同時發生。一旦一端開始發送消息,另外一端要接收完整個消息才能響應它,因此咱們沒法也無須將一個消息切成小塊獨立發送,也沒有辦法進行流量控制。服務器
客戶端用一個單獨的數據包將查詢請求發送給服務器,因此當查詢語句很長的時候,須要設置max_allowed_packet參數。可是須要注意的是,若是查詢實在是太大,服務端會拒絕接收更多數據並拋出異常。數據結構
與之相反的是,服務器響應給用戶的數據一般會不少,由多個數據包組成。可是當服務器響應客戶端請求時,客戶端必須完整的接收整個返回結果,而不能簡單的只取前面幾條結果,而後讓服務器中止發送。於是在實際開發中,儘可能保持查詢簡單且只返回必需的數據,減少通訊間數據包的大小和數量是一個很是好的習慣,這也是查詢中儘可能避免使用SELECT *以及加上LIMIT限制的緣由之一。架構
在解析一個查詢語句前,若是查詢緩存是打開的,那麼MySQL會檢查這個查詢語句是否命中查詢緩存中的數據。若是當前查詢剛好命中查詢緩存,在檢查一次用戶權限後直接返回緩存中的結果。這種狀況下,查詢不會被解析,也不會生成執行計劃,更不會執行。
MySQL將緩存存放在一個引用表(不要理解成table,能夠認爲是相似於HashMap的數據結構),經過一個哈希值索引,這個哈希值經過查詢自己、當前要查詢的數據庫、客戶端協議版本號等一些可能影響結果的信息計算得來。因此兩個查詢在任何字符上的不一樣(例如:空格、註釋),都會致使緩存不會命中。
若是查詢中包含任何用戶自定義函數、存儲函數、用戶變量、臨時表、mysql庫中的系統表,其查詢結果都不會被緩存。好比函數NOW()或者CURRENT_DATE()會由於不一樣的查詢時間,返回不一樣的查詢結果,再好比包含CURRENT_USER或者CONNECION_ID()的查詢語句會由於不一樣的用戶而返回不一樣的結果,將這樣的查詢結果緩存起來沒有任何的意義。
既然是緩存,就會失效,那查詢緩存什麼時候失效呢?MySQL的查詢緩存系統會跟蹤查詢中涉及的每一個表,若是這些表(數據或結構)發生變化,那麼和這張表相關的全部緩存數據都將失效。正由於如此,在任何的寫操做時,MySQL必須將對應表的全部緩存都設置爲失效。若是查詢緩存很是大或者碎片不少,這個操做就可能帶來很大的系統消耗,甚至致使系統僵死一下子。並且查詢緩存對系統的額外消耗也不只僅在寫操做,讀操做也不例外:
- 任何的查詢語句在開始以前都必須通過檢查,即便這條SQL語句永遠不會命中緩存
- 若是查詢結果能夠被緩存,那麼執行完成後,會將結果存入緩存,也會帶來額外的系統消耗
基於此,咱們要知道並非什麼狀況下查詢緩存都會提升系統性能,緩存和失效都會帶來額外消耗,只有當緩存帶來的資源節約大於其自己消耗的資源時,纔會給系統帶來性能提高。但要如何評估打開緩存是否可以帶來性能提高是一件很是困難的事情,也不在本文討論的範疇內。若是系統確實存在一些性能問題,能夠嘗試打開查詢緩存,並在數據庫設計上作一些優化,好比:
- 用多個小表代替一個大表,注意不要過分設計
- 批量插入代替循環單條插入
- 合理控制緩存空間大小,通常來講其大小設置爲幾十兆比較合適
- 能夠經過SQL_CACHE和SQL_NO_CACHE來控制某個查詢語句是否須要進行緩存
最後的忠告是不要輕易打開查詢緩存,特別是寫密集型應用。若是你實在是忍不住,能夠將query_cache_type設置爲DEMAND,這時只有加入SQL_CACHE的查詢纔會走緩存,其餘查詢則不會,這樣能夠很是自由地控制哪些查詢須要被緩存。
固然查詢緩存系統自己是很是複雜的,這裏討論的也只是很小的一部分,其餘更深刻的話題,好比:緩存是如何使用內存的?如何控制內存的碎片化?事務對查詢緩存有何影響等等,讀者能夠自行閱讀相關資料,這裏權當拋磚引玉吧。
MySQL經過關鍵字將SQL語句進行解析,並生成一顆對應的解析樹。這個過程解析器主要經過語法規則來驗證和解析。好比SQL中是否使用了錯誤的關鍵字或者關鍵字的順序是否正確等等。預處理則會根據MySQL規則進一步檢查解析樹是否合法。好比檢查要查詢的數據表和數據列是否存在等等。
通過前面的步驟生成的語法樹被認爲是合法的了,而且由優化器將其轉化成查詢計劃。多數狀況下,一條查詢能夠有不少種執行方式,最後都返回相應的結果。優化器的做用就是找到這其中最好的執行計劃。
MySQL使用基於成本的優化器,它嘗試預測一個查詢使用某種執行計劃時的成本,並選擇其中成本最小的一個。在MySQL能夠經過查詢當前會話的last_query_cost的值來獲得其計算當前查詢的成本。
mysql> select * from t_message limit 10;
...省略結果集
mysql> show status like 'last_query_cost';
+-----------------+-------------+
| Variable_name | Value |
+-----------------+-------------+
| Last_query_cost | 6391.799000 |
+-----------------+-------------+
複製代碼
示例中的結果表示優化器認爲大概須要作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整個查詢執行過程,總的來講分爲5個步驟:
- 客戶端向MySQL服務器發送一條查詢請求
- 服務器首先檢查查詢緩存,若是命中緩存,則馬上返回存儲在緩存中的結果。不然進入下一階段
- 服務器進行SQL解析、預處理、再由優化器生成對應的執行計劃
- MySQL根據執行計劃,調用存儲引擎的API來執行查詢
- 將結果返回給客戶端,同時緩存查詢結果