MySQL查询过程及Scheme设计与数据类型优化
MySQL查詢過程
我們總是希望MySQL能夠獲得更高的查詢性能,最好的辦法是弄清楚MySQL是如何優化和執行查詢的。一旦理解了這一點,就會發現:很多的查詢優化工作實際上就是遵循一些原則讓MySQL的優化器能夠按照預想的合理方式運行而已。
當向MySQL發送一個請求的時候,MySQL到底做了些什么呢?
客戶端/服務端通信協議
MySQL客戶端/服務端通信協議是“半雙工”的:在任一時刻,要么是服務器向客戶端發送數據,要么是客戶端向服務器發送數據,這兩個動作不能同時發生。一旦一端開始發送消息,另一端要接收完整個消息才能響應它,所以我們無法也無須將一個消息切成小塊獨立發送,也沒有辦法進行流量控制。
客戶端用一個單獨的數據包將查詢請求發送給服務器,所以當查詢語句很長的時候,需要設置max_allowed_packet參數。但是需要注意的是,如果查詢實在是太大,服務端會拒絕接收更多數據并拋出異常。
與之相反的是,服務器響應給用戶的數據通常會很多,由多個數據包組成。但是當服務器響應客戶端請求時,客戶端必須完整的接收整個返回結果,而不能簡單的只取前面幾條結果,然后讓服務器停止發送。因而在實際開發中,盡量保持查詢簡單且只返回必需的數據,減小通信間數據包的大小和數量是一個非常好的習慣,這也是查詢中盡量避免使用SELECT *以及加上LIMIT限制的原因之一。
查詢緩存
在解析一個查詢語句前,如果查詢緩存是打開的,那么MySQL會檢查這個查詢語句是否命中查詢緩存中的數據。如果當前查詢恰好命中查詢緩存,在檢查一次用戶權限后直接返回緩存中的結果。這種情況下,查詢不會被解析,也不會生成執行計劃,更不會執行。
MySQL將緩存存放在一個引用表(不要理解成table,可以認為是類似于HashMap的數據結構),通過一個哈希值索引,這個哈希值通過查詢本身、當前要查詢的數據庫、客戶端協議版本號等一些可能影響結果的信息計算得來。所以兩個查詢在任何字符上的不同(例如:空格、注釋),都會導致緩存不會命中。
如果查詢中包含任何用戶自定義函數、存儲函數、用戶變量、臨時表、mysql庫中的系統表,其
查詢結果
都不會被緩存。比如函數NOW()或者CURRENT_DATE()會因為不同的查詢時間,返回不同的查詢結果,再比如包含CURRENT_USER或者CONNECION_ID()的查詢語句會因為不同的用戶而返回不同的結果,將這樣的查詢結果緩存起來沒有任何的意義。
既然是緩存,就會失效,那查詢緩存何時失效呢?MySQL的查詢緩存系統會跟蹤查詢中涉及的每個表,如果這些表(數據或結構)發生變化,那么和這張表相關的所有緩存數據都將失效。正因為如此,在任何的寫操作時,MySQL必須將對應表的所有緩存都設置為失效。如果查詢緩存非常大或者碎片很多,這個操作就可能帶來很大的系統消耗,甚至導致系統僵死一會兒。而且查詢緩存對系統的額外消耗也不僅僅在寫操作,讀操作也不例外:
任何的查詢語句在開始之前都必須經過檢查,即使這條SQL語句永遠不會命中緩存
如果查詢結果可以被緩存,那么執行完成后,會將結果存入緩存,也會帶來額外的系統消耗
基于此,我們要知道并不是什么情況下查詢緩存都會提高系統性能,緩存和失效都會帶來額外消耗,只有當緩存帶來的資源節約大于其本身消耗的資源時,才會給系統帶來性能提升。但要如何評估打開緩存是否能夠帶來性能提升是一件非常困難的事情,也不在本文討論的范疇內。如果系統確實存在一些性能問題,可以嘗試打開查詢緩存,并在數據庫設計上做一些優化,比如:
當然查詢緩存系統本身是非常復雜的,這里討論的也只是很小的一部分,其他更深入的話題,比如:緩存是如何使用內存的?如何控制內存的碎片化?事務對查詢緩存有何影響等等,讀者可以自行閱讀相關資料,這里權當拋磚引玉吧。
語法解析和預處理
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整個查詢執行過程,總的來說分為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執行大部分修改表結果操作的方法是用新的結構創建一個張空表,從舊表中查出所有的數據插入新表,然后再刪除舊表。尤其當內存不足而表又很大,而且還有很大索引的情況下,耗時更久。當然有一些奇技淫巧可以解決這個問題,有興趣可自行查閱。創建高性能索引
索引是提高MySQL查詢性能的一個重要途徑,但過多的索引可能會導致過高的磁盤使用率以及過高的內存占用,從而影響應用程序的整體性能。應當盡量避免事后才想起添加索引,因為事后可能需要監控大量的SQL才能定位到問題所在,而且添加索引的時間肯定是遠大于初始添加索引所需要的時間,可見索引的添加也是非常有技術含量的。
接下來將向你展示一系列創建高性能索引的策略,以及每條策略其背后的工作原理。但在此之前,先了解與索引相關的一些算法和數據結構,將有助于更好的理解后文的內容。
總結
以上是生活随笔為你收集整理的MySQL查询过程及Scheme设计与数据类型优化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MySQL逻辑架构
- 下一篇: MySQL索引相关的数据结构和算法