跳轉到

SELECT 執行流程

MySQL 架構

img

Server 層

負責建立連接、分析和執行 SQL。主要包括連接器,查詢緩存、解析器、預處理器、優化器、執行器。

內置函數(日期、時間、數學和加密函數)和所有跨儲存引擎的功能(SP、Trigger、View)也都是在 Server 層實現。

Storage Enigne 層

負責資料的儲存和提取。支援 InnoDB、MyISAM、etc. 不同的 Storage Engine 共用一個 Server 層。

我們常說的索引資料結構,就是在這實現的,不同的 Storage Engine 支援的索引類型也不同,像 InnoDB 預設支援的索引類型是 B+ 樹。

1. 連接器

如果你在 Linux 操作系統裡要使用 MySQL,那你第一步肯定是要先連接 MySQL 服務,然後才能執行 SQL 語句。

連接的過程需要先經過 TCP 三次握手,因為 MySQL 是基於 TCP 協議進行傳輸的,如果 MySQL 服務並沒有啟動,則會收到如下的報錯:

如果 MySQL 服務正常運行,完成 TCP 連接的建立後,連接器就要開始驗證你的用戶名和密碼,如果用戶名或密碼不對,就收到一個 Access denied for user 的錯誤。

如果用戶密碼都沒有問題,連接器就會獲取該用戶的權限,然後保存起來,後續該用戶在此連接裡的任何操作,都會基於連接開始時讀到的權限進行權限邏輯的判斷。

Question

如何查看 MySQL 被多少個客戶端連接?

如果你想知道當前 MySQL 服務被多少個客戶端連接了,你可以執行 show processlist 命令進行查看。

比如上圖的顯示結果,共有兩個用戶名為 root 的用戶連接了 MySQL 服務,其中 id 為 6 的用戶的 Command 列的狀態為 Sleep,這意味著該用戶連接完 MySQL 服務就沒有再執行過任何命令,也就是說這是一個空閒的連接,並且空閒的時長是 736 秒。

Question

空閒的連接會一直佔用著嗎?

當然不是了,MySQL 定義了空閒連接的最大空閒時長,由 wait_timeout 參數控制,默認值是 8 小時(28880 秒),如果空閒連接超過了這個時間,連接器就會自動將它斷開。

mysql> show variables like 'wait_timeout';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| wait_timeout  | 28800 |
+---------------+-------+
1 row in set (0.00 sec)

我們自己也可以手動斷開空閒的連接,使用的是 kill connection +id 的命令。

mysql> kill connection +6;
Query OK, 0 rows affected (0.00 sec)

一個處於空閒狀態的連接被服務端主動斷開後,這個客戶端並不會馬上知道,等到客戶端在發起下一個請求的時候,才會收到這樣的報錯 ERROR 2013 (HY000): Lost connection to MySQL server during query

Question

MySQL 的連接數有限制嗎?

MySQL 服務支持的最大連接數由 max_connections 參數控制,MySQL 服務默認是 151 個,超過這個值,系統就會拒絕接下來的連接請求,並報錯提示 Too many connections

mysql> show variables like 'max_connections';
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| max_connections | 151   |
+-----------------+-------+
1 row in set (0.00 sec)

MySQL 的連接也跟 HTTP 一樣,有短連接和長連接的概念。

使用長連接的好處就是可以減少建立連接和斷開連接的過程,所以一般是推薦使用長連接。

但是如果長連接累計過多,將導致 MySQL 服務佔用記憶體太大,有可能會被系統強制殺掉,發生 MySQL 服務異常重啟的現象。

Question

怎麼解決長連接佔用記憶體的問題?

有兩種解決方式:

  • 定期斷開長連接。既然斷開連接後就會釋放連接佔用的記憶體資源,那麼我們可以定期斷開長連接。

  • 客戶端主動重置連接。MySQL 5.7 版本實現 mysql_reset_connection() 函數的接口,這是接口函數不是命令,當客戶端執行了一個很大的操作後,在代碼裡調用 mysql_reset_connection 函數來重置連接,達到釋放記憶體的效果。這個過程不需要重連和重新做權限驗證,但是會將連接恢復到剛剛創建完時的狀態。

2. 查詢緩存

Warning

Deprecated since MySQL 8.0

連接器的工作完成後,客戶端就可以向 MySQL 服務發送 SQL 語句了,MySQL 服務收到 SQL 語句後,就會解析出 SQL 語句的第一個字段,看看是什麼類型的語句。

如果 SQL 是查詢語句(select 語句),MySQL 就會先去查詢緩存( Query Cache )裡找資料,看看之前有沒有執行過這一條命令,這個查詢緩存是以 key-value 形式保存在記憶體中的,key 為 SQL 查詢語句,value 為 SQL 語句查詢的結果。

如果查詢的語句命中緩存,就會直接返回 value 給客戶端。否則就往下繼續執行,執行完後會將結果存入緩存。

對於更新比較頻繁的表,緩存的命中率是很低的,因為只要一個表有更新操作,這個表的緩存就會被清空。如果剛緩存了一個查詢結果很大的資料,還沒被使用,又馬上有新的操作更新表,緩存就被清空了。

因此,MySQL 8.0 版本直接將查詢緩存刪掉了。MySQL 8.0 之前的版本,如果想關閉緩存,我們可將參數 query_cache_type 設置成 DEMAND

3. 解析器

在正式執行 SQL 查詢語句之前, MySQL 會先對 SQL 語句做解析,這個工作交由「解析器」來完成。

Question

解析器做了什麼事情?

  • 詞法分析。MySQL 會根據輸入的字符串識別出關鍵字,構建 SQL 語法樹,方便後面模塊獲取 SQL 類型、表名、字段名、 where 條件。

  • 語法分析。根據詞法分析的結果,語法解析器會根據語法規則,判斷 SQL 語句是否滿足 MySQL 語法。

如果我們輸入的 SQL 語句語法不對,就會在解析器這個階段報錯。比如,下面這條查詢語句,把 from 寫成了 form,這時 MySQL 解析器就會報錯。

4. 預處理器

Question

預處理器做了什麼事情?

  • 檢查 SQL 查詢語句中的表或者字段是否存在
  • select * 中的 * 符號,擴展為表上的所有列

5. 優化器

經過預處理階段後,會由優化器負責制定 SQL 查詢語句的執行方案

Info

在表裡面有多個索引的時候,優化器會考量查詢成本,決定要使用哪個索引。

想知道優化器選擇了哪個索引,可以在查詢語句最前面加上 explain ,這樣就會輸出 SQL 語句的執行計劃

6. 執行器

確定了執行方案,接下來 MySQL 就真正開始執行語句了。在執行的過程中,執行器會和 Storage Engine 互動,互動是以記錄為單位的。

以下介紹兩種執行過程:

  • 主鍵索引查詢
  • 全表掃描

主鍵索引查詢

select * from product where id = 1;

這條查詢語句的查詢條件用到了主鍵索引,而且是等值查詢,同時主鍵 id 是唯一,不會有 id 相同的記錄,所以優化器決定選用訪問類型為 const 進行查詢,也就是使用主鍵索引查詢一條記錄,那麼執行器與儲存引擎的執行流程是這樣的:

  • 執行器第一次查詢,會調用 read_first_record 函數指針指向的函數,因為優化器選擇的訪問類型為 const,這個函數指針被指向為 InnoDB 引擎索引查詢的接口,把條件 id = 1 交給儲存引擎,讓儲存引擎定位符合條件的第一條記錄
  • 儲存引擎通過主鍵索引的 B+ 樹結構定位到 id = 1 的第一條記錄,如果記錄是不存在的,就會向執行器上報記錄找不到的錯誤,然後查詢結束。如果記錄是存在的,就會將記錄返回給執行器
  • 執行器從儲存引擎讀到記錄後,接著判斷記錄是否符合查詢條件,如果符合則發送給客戶端,如果不符合則跳過該記錄。查一次,但是這次因為不是第一次查詢了,所以會調用 read_record 函數指針指向的函數,因為優化器選擇的訪問類型為 const,這個函數指針被指向為一個永遠返回 -1 的函數,所以當調用該函數的時候,執行器就退出循環,也就是結束查詢了

全表掃描

select * from product where name = 'iphone';

這條查詢語句的查詢條件沒有用到索引,所以優化器決定選用訪問類型為 ALL 進行查詢,也就是全表掃描的方式查詢,那麼這時執行器與儲存引擎的執行流程是這樣的:

  • 執行器第一次查詢,會調用 read_first_record 函數指針指向的函數,因為優化器選擇的訪問類型為 all,這個函數指針被指向為 InnoDB 引擎全掃描的接口,讓儲存引擎讀取表中的第一條記錄
  • 執行器會判斷讀到的這條記錄的 name 是不是 iphone,如果不是則跳過;如果是則將記錄發給客戶的(是的沒錯,Server 層每從儲存引擎讀到一條記錄就會發送給客戶端,之所以客戶端顯示的時候是直接顯示所有記錄的,是因為客戶端是等查詢語句查詢完成後,才會顯示出所有的記錄
  • 執行器查詢的過程是一個 while 循環,所以還會再查一次,會調用 read_record 函數指針指向的函數,因為優化器選擇的訪問類型為 all,read_record 函數指針指向的還是 InnoDB 引擎全掃描的接口,所以接著向儲存引擎層要求繼續讀剛才那條記錄的下一條記錄,儲存引擎把下一條記錄取出後就將其返回給執行器(Server 層),執行器繼續判斷條件,不符合查詢條件即跳過該記錄,否則發送到客戶端
  • 一直重複上述過程,直到儲存引擎把表中的所有記錄讀完,然後向執行器(Server 層) 返回了讀取完畢的信息
  • 執行器收到儲存引擎報告的查詢完畢的信息,退出循環,停止查詢

Reference