當咱們輸入一條 SQL 查詢語句時,發生了什麼?

當咱們輸入一條 SQL 查詢語句時,發生了什麼?

 

咱們常常說,看一個事兒千萬不要直接陷入細節裏,你應該先鳥瞰其全貌,這樣可以幫助你從高維度理解問題。一樣,對於 MySQL 的學習也是這樣。平時咱們使用數據庫,看到的一般都是一個總體。好比,你有個最簡單的表,表裏只有一個 ID 字段,在執行下面這個查詢語句時:mysql

 複製代碼sql

mysql> select * from T where ID= 10 ;數據庫

咱們看到的只是輸入一條語句,返回一個結果,殊不知道這條語句在 MySQL 內部的執行過程。後端

因此今天我想和你一塊兒把 MySQL 拆解一下,看看裏面都有哪些「零件」,但願藉由這個拆解過程,讓你對 MySQL 有更深刻的理解。這樣當咱們碰到 MySQL 的一些異常或者問題時,就可以直戳本質,更爲快速地定位並解決問題。緩存

下面我給出的是 MySQL 的基本架構示意圖,從中你能夠清楚地看到 SQL 語句在 MySQL 的各個功能模塊中的執行過程。服務器

當咱們輸入一條 SQL 查詢語句時,發生了什麼?

 

MySQL 的邏輯架構圖架構

大致來講,MySQL 能夠分爲 Server 層和存儲引擎層兩部分。函數

Server 層包括鏈接器、查詢緩存、分析器、優化器、執行器等,涵蓋 MySQL 的大多數核心服務功能,以及全部的內置函數(如日期、時間、數學和加密函數等),全部跨存儲引擎的功能都在這一層實現,好比存儲過程、觸發器、視圖等。工具

而存儲引擎層負責數據的存儲和提取。其架構模式是插件式的,支持 InnoDB、MyISAM、Memory 等多個存儲引擎。如今最經常使用的存儲引擎是 InnoDB,它從 MySQL 5.5.5 版本開始成爲了默認存儲引擎。學習

也就是說,你執行 create table 建表的時候,若是不指定引擎類型,默認使用的就是 InnoDB。不過,你也能夠經過指定存儲引擎的類型來選擇別的引擎,好比在 create table 語句中使用 engine=memory, 來指定使用內存引擎建立表。不一樣存儲引擎的表數據存取方式不一樣,支持的功能也不一樣,在後面的文章中,咱們會討論到引擎的選擇。

從圖中不難看出,不一樣的存儲引擎共用一個 Server 層,也就是從鏈接器到執行器的部分。你能夠先對每一個組件的名字有個印象,接下來我會結合開頭提到的那條 SQL 語句,帶你走一遍整個執行流程,依次看下每一個組件的做用。

鏈接器

第一步,你會先鏈接到這個數據庫上,這時候接待你的就是鏈接器。鏈接器負責跟客戶端創建鏈接、獲取權限、維持和管理鏈接。鏈接命令通常是這麼寫的:

 複製代碼

mysql -h $ip -P $port -u $user - p

輸完命令以後,你就須要在交互對話裏面輸入密碼。雖然密碼也能夠直接跟在 -p 後面寫在命令行中,但這樣可能會致使你的密碼泄露。若是你連的是生產服務器,強烈建議你不要這麼作。

鏈接命令中的 mysql 是客戶端工具,用來跟服務端創建鏈接。在完成經典的 TCP 握手後,鏈接器就要開始認證你的身份,這個時候用的就是你輸入的用戶名和密碼。

  • 若是用戶名或密碼不對,你就會收到一個 "Access denied for user" 的錯誤,而後客戶端程序結束執行。
  • 若是用戶名密碼認證經過,鏈接器會到權限表裏面查出你擁有的權限。以後,這個鏈接裏面的權限判斷邏輯,都將依賴於此時讀到的權限。

這就意味着,一個用戶成功創建鏈接後,即便你用管理員帳號對這個用戶的權限作了修改,也不會影響已經存在鏈接的權限。修改完成後,只有再新建的鏈接纔會使用新的權限設置。

鏈接完成後,若是你沒有後續的動做,這個鏈接就處於空閒狀態,你能夠在 show processlist 命令中看到它。文本中這個圖是 show processlist 的結果,其中的 Command 列顯示爲「Sleep」的這一行,就表示如今系統裏面有一個空閒鏈接。

當咱們輸入一條 SQL 查詢語句時,發生了什麼?

 

客戶端若是太長時間沒動靜,鏈接器就會自動將它斷開。這個時間是由參數 wait_timeout 控制的,默認值是 8 小時。

若是在鏈接被斷開以後,客戶端再次發送請求的話,就會收到一個錯誤提醒: Lost connection to MySQL server during query。這時候若是你要繼續,就須要重連,而後再執行請求了。

數據庫裏面,長鏈接是指鏈接成功後,若是客戶端持續有請求,則一直使用同一個鏈接。短鏈接則是指每次執行完不多的幾回查詢就斷開鏈接,下次查詢再從新創建一個。

創建鏈接的過程一般是比較複雜的,因此我建議你在使用中要儘可能減小創建鏈接的動做,也就是儘可能使用長鏈接。

可是所有使用長鏈接後,你可能會發現,有些時候 MySQL 佔用內存漲得特別快,這是由於 MySQL 在執行過程當中臨時使用的內存是管理在鏈接對象裏面的。這些資源會在鏈接斷開的時候才釋放。因此若是長鏈接累積下來,可能致使內存佔用太大,被系統強行殺掉(OOM),從現象看就是 MySQL 異常重啓了。

怎麼解決這個問題呢?你能夠考慮如下兩種方案。

  1. 按期斷開長鏈接。使用一段時間,或者程序裏面判斷執行過一個佔用內存的大查詢後,斷開鏈接,以後要查詢再重連。
  2. 若是你用的是 MySQL 5.7 或更新版本,能夠在每次執行一個比較大的操做後,經過執行 mysql_reset_connection 來從新初始化鏈接資源。這個過程不須要重連和從新作權限驗證,可是會將鏈接恢復到剛剛建立完時的狀態。

查詢緩存

鏈接創建完成後,你就能夠執行 select 語句了。執行邏輯就會來到第二步:查詢緩存。

MySQL 拿到一個查詢請求後,會先到查詢緩存看看,以前是否是執行過這條語句。以前執行過的語句及其結果可能會以 key-value 對的形式,被直接緩存在內存中。key 是查詢的語句,value 是查詢的結果。若是你的查詢可以直接在這個緩存中找到 key,那麼這個 value 就會被直接返回給客戶端。

若是語句不在查詢緩存中,就會繼續後面的執行階段。執行完成後,執行結果會被存入查詢緩存中。你能夠看到,若是查詢命中緩存,MySQL 不須要執行後面的複雜操做,就能夠直接返回結果,這個效率會很高。

可是大多數狀況下我會建議你不要使用查詢緩存,爲何呢?由於查詢緩存每每弊大於利。

查詢緩存的失效很是頻繁,只要有對一個表的更新,這個表上全部的查詢緩存都會被清空。所以極可能你費勁地把結果存起來,還沒使用呢,就被一個更新全清空了。對於更新壓力大的數據庫來講,查詢緩存的命中率會很是低。除非你的業務就是有一張靜態表,很長時間纔會更新一次。好比,一個系統配置表,那這張表上的查詢才適合使用查詢緩存。

好在 MySQL 也提供了這種「按需使用」的方式。你能夠將參數 query_cache_type 設置成 DEMAND,這樣對於默認的 SQL 語句都不使用查詢緩存。而對於你肯定要使用查詢緩存的語句,能夠用 SQL_CACHE 顯式指定,像下面這個語句同樣:

 複製代碼

mysql> select SQL_CACHE * from T where ID= 10 ;

須要注意的是,MySQL 8.0 版本直接將查詢緩存的整塊功能刪掉了,也就是說 8.0 開始完全沒有這個功能了。

分析器

若是沒有命中查詢緩存,就要開始真正執行語句了。首先,MySQL 須要知道你要作什麼,所以須要對 SQL 語句作解析。

分析器先會作「詞法分析」。你輸入的是由多個字符串和空格組成的一條 SQL 語句,MySQL 須要識別出裏面的字符串分別是什麼,表明什麼。

MySQL 從你輸入的 "select" 這個關鍵字識別出來,這是一個查詢語句。它也要把字符串「T」識別成「表名 T」,把字符串「ID」識別成「列 ID」。

作完了這些識別之後,就要作「語法分析」。根據詞法分析的結果,語法分析器會根據語法規則,判斷你輸入的這個 SQL 語句是否知足 MySQL 語法。

若是你的語句不對,就會收到「You have an error in your SQL syntax」的錯誤提醒,好比下面這個語句 select 少打了開頭的字母「s」。

 複製代碼

mysql> elect * from t where ID= 1 ;

ERROR 1064 ( 42000 ): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'elect * from t where ID= 1 ' at line 1

通常語法錯誤會提示第一個出現錯誤的位置,因此你要關注的是緊接「use near」的內容。

優化器

通過了分析器,MySQL 就知道你要作什麼了。在開始執行以前,還要先通過優化器的處理。

優化器是在表裏面有多個索引的時候,決定使用哪一個索引;或者在一個語句有多表關聯(join)的時候,決定各個表的鏈接順序。好比你執行下面這樣的語句,這個語句是執行兩個表的 join:

 複製代碼

mysql> select * from t1 join t2 using(ID) where t1 .c= 10 and t2 .d= 20 ;

  • 既能夠先從表 t1 裏面取出 c=10 的記錄的 ID 值,再根據 ID 值關聯到表 t2,再判斷 t2 裏面 d 的值是否等於 20。
  • 也能夠先從表 t2 裏面取出 d=20 的記錄的 ID 值,再根據 ID 值關聯到 t1,再判斷 t1 裏面 c 的值是否等於 10。

這兩種執行方法的邏輯結果是同樣的,可是執行的效率會有不一樣,而優化器的做用就是決定選擇使用哪個方案。

優化器階段完成後,這個語句的執行方案就肯定下來了,而後進入執行器階段。若是你還有一些疑問,好比優化器是怎麼選擇索引的,有沒有可能選擇錯等等,不要緊,我會在後面的文章中單獨展開說明優化器的內容。

執行器

MySQL 經過分析器知道了你要作什麼,經過優化器知道了該怎麼作,因而就進入了執行器階段,開始執行語句。

開始執行的時候,要先判斷一下你對這個表 T 有沒有執行查詢的權限,若是沒有,就會返回沒有權限的錯誤,以下所示。

 複製代碼

mysql> select * from T where ID= 10 ;

ERROR 1142 ( 42000 ): SELECT command denied to user 'b' @ 'localhost' for table 'T'

若是有權限,就打開表繼續執行。打開表的時候,執行器就會根據表的引擎定義,去使用這個引擎提供的接口。

好比咱們這個例子中的表 T 中,ID 字段沒有索引,那麼執行器的執行流程是這樣的:

  1. 調用 InnoDB 引擎接口取這個表的第一行,判斷 ID 值是否是 10,若是不是則跳過,若是是則將這行存在結果集中;
  2. 調用引擎接口取「下一行」,重複相同的判斷邏輯,直到取到這個表的最後一行。
  3. 執行器將上述遍歷過程當中全部知足條件的行組成的記錄集做爲結果集返回給客戶端。

至此,這個語句就執行完成了。

對於有索引的表,執行的邏輯也差很少。第一次調用的是「取知足條件的第一行」這個接口,以後循環取「知足條件的下一行」這個接口,這些接口都是引擎中已經定義好的。

你會在數據庫的慢查詢日誌中看到一個 rows_examined 的字段,表示這個語句執行過程當中掃描了多少行。這個值就是在執行器每次調用引擎獲取數據行的時候累加的。

在有些場景下,執行器調用一次,在引擎內部則掃描了多行,所以 引擎掃描行數跟 rows_examined 並非徹底相同的 。

歡迎你們加入Java後端高級技術羣:479499375。

相關文章
相關標籤/搜索