SQL語句是如何執行的?

目錄

1.查詢語句是如何執行的

1.1 MySQL的邏輯架構圖

1.1.1 連接器

1.1.2 查詢緩存

1.1.3 分析器

1.1.4 優化器

1.1.5 執行器

1.2 問題

2.更新語句是如何執行的

2.1 redo log

2.2 binlog

2.3 兩階段提交


1.查詢語句是如何執行的

假設我們有個最簡單的表,表裏只有一個ID字段,再執行下面一條查詢語句:

mysql> select * from T where ID=10;

我們看到的只是輸入一條語句,返回一個結果,卻不知道這條語句在MySQL內部的執行過程。爲了解釋這個問題,我們首先看一下MySQL的基本架構示意圖,從中我們可以清楚的看到SQL語句在MySQL的各個功能模塊中的執行過程。

1.1 MySQL的邏輯架構圖

MySQL的邏輯架構圖如下圖所示:

大體上來說,MySQL可以分爲Server層和存儲引擎層兩部分。

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

存儲引擎層主要負責數據的存儲和提取。

1.1.1 連接器

連接器負責跟客戶端建立連接、獲取權限、維持和管理連接。連接命令一般爲:

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

輸入命令之後,就需要在交互對話裏面輸入密碼。如果用戶名和密碼認證通過,連接器就會到權限表裏面查出賬號擁有的權限。之後,這個連接裏面的權限判斷邏輯,都將依賴於此時讀到的權限。這就意味着,一個用戶成功建立連接後,即使用管理員賬號對這個用戶的權限做了修改,也不會影響已經存在連接的權限。修改完成後,只有再新建的連接纔會使用新的權限設置。

連接成功以後,如果沒有後續動作,這個連接就會處於空閒狀態,我們可以在show processlist命令中看到它。執行結果如下:

mysql> show processlist;
+---------+------+---------------------+---------+---------+------+----------+------------------+
| Id      | User | Host                | db      | Command | Time | State    | Info             |
+---------+------+---------------------+---------+---------+------+----------+------------------+
| 1050383 | root | 101.86.136.19:17307 | mysql   | Sleep   |  611 |          | NULL             |
| 1050384 | root | 101.86.136.19:17332 | my-test | Sleep   |  726 |          | NULL             |
| 1050385 | root | 101.86.136.19:17333 | my-test | Sleep   |  726 |          | NULL             |
| 1050386 | root | 101.86.136.19:17765 | NULL    | Query   |    0 | starting | show processlist |
+---------+------+---------------------+---------+---------+------+----------+------------------+
4 rows in set (0.04 sec)

其中的Command列顯示爲“Sleep”,表示現在系統中有一個空閒連接。客戶端如果超過一段時間沒有操作,連接器就會自動將它斷開。這個時間是由參數wait_timeout控制的,默認值是8小時。

數據庫裏面,長連接是指連接成功後,如果客戶端持續有請求,則一直使用同一個連接。短連接則是每次執行完很少的幾次查詢就斷開連接,下次查詢再重新建立一個。連接的建立過程,通常是比較複雜的,所以一般我們儘量使用長連接。

但是如果全部使用長連接後,有些時候MySQL佔用內存漲的就特別厲害,這是因爲MySQL在執行過程中臨時使用的內存管理在連接對象裏面的。這些資源會在連接斷開的時候才釋放。所以如果長連接累積下來,可能導致內存佔用很大,被系統強行殺掉(OOM),從現象看就是MySQL異常重啓了。

如果解決這個問題呢?我們可以考慮如下方案:

  1. 定期斷開長連接。使用一段時間,或者程序裏面判斷執行過一個佔用內存的大查詢後,斷開連接,之後要查詢再重連。
  2. 如果我們使用的是MySQL5.7或者更新版本,可以在每次執行一個比較大的操作後,通過執行mysql_reset_connection來重新初始化連接資源。這個過程不需要重連和重新做權限驗證,但是會將連接恢復到剛剛創建完時的狀態。

1.1.2 查詢緩存

MySQL拿到查詢請求後,會先到查詢緩存中去查找有沒有執行過該SQL語句。如果已經執行過,就會將緩存結果直接返回給客戶端。如果不在查詢緩存中,就會執行後面的操作;執行完成後,執行結果會被存入查詢緩存中。

但是大多數情況下,通常不建議開啓查詢緩存,因爲查詢緩存往往弊大於利。查詢緩存的失效是非常頻繁的,只要有對一個表的更新,這個表上所有的查詢緩存都會被清空。對於更新比較頻繁地數據庫來說,查詢緩存的命中率是非常低效的。除非你的業務是一張靜態表,很長時間纔會更新一次。

MySQL8.0版本已經將查詢緩存的整塊功能刪除了,也就是說8.0版本已經徹底沒有這個功能了。

1.1.3 分析器

分析器會先做“詞法分析”,SQL是由多個字符串和空格組成的,MySQL需要識別出裏面的字符串分別是什麼,代表什麼。比如,MySQL從輸入的“select”關鍵字識別出來這個一個查詢語句;把字符串“T”識別成“表名T”,把字符串“ID”識別成“列ID”。

根據詞法分析的結果,語法分析器會根據語法規則,判斷輸入的這個SQL是否滿足MySQL語法。如果語法不對,就會收到“You have an error in your SQL syntax”的錯誤提示。

1.1.4 優化器

優化器是在表裏面有多個索引的時候,決定使用哪個索引;或者在一個語句有多表關聯的時候,決定各個表的連接順序。比如執行下面的兩個表的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。這兩種執行方法的邏輯結果是一致的,但是執行效率會有不同,而優化器的作用就是決定選擇使用哪一個方案。

1.1.5 執行器

MySQL通過分析器知道了你要做什麼,通過優化器知道了該怎麼做,於是進入了執行器階段,開始執行語句。開始執行的時候,要先判斷對該表有沒有查詢權限,如果沒有就返回沒有權限的錯誤。如果有,則打開表繼續執行。

1.2 問題

如果表T中沒有字段k,而執行了語句select * from T where k=1,那肯定會報“不存在這個列”的錯誤,那這個錯誤在哪個階段報出來的呢?

答:分析器。MySQL會在分析階段判斷語句是否正確,表是否存在,列是否存在等。

2.更新語句是如何執行的

我們首先從一張表的更新語句說起,下面是這個表的建表語句:

create table T(ID int primary key,c int)

如果將ID=2這一行的值加1,SQL語句會這樣寫:

update T set c=c+1 where ID=2

首先可以確定的是查詢語句的那一套流程,在更新語句同樣適用:

  1. 在一個表中有更新的時候,跟這個表有關的查詢緩存都會失效,所以這條語句首先會把表T上的所有緩存結果都清空。這也就是我們一般不建議使用查詢緩存的原因。
  2. 接下來,分析器會通過詞法和語法解析這條更新語句;優化器決定要使用ID這個索引;最後,執行器負責具體執行,找到這一行,進行更新。

與查詢流程不一樣的是,更新流程還涉及兩個重要的日誌模塊,redo log(重做日誌)和binlog(歸檔日誌)。

2.1 redo log

在MySQL中如果每次更新操作都需要寫進磁盤,然後磁盤要找到對應的記錄,然後再更新,整個過程IO成本、查找成本都很高。爲了解決這個問題,MySQL的設計者引入了redo log。具體來說,當有一條記錄需要更新的時候,InnoDB引擎會先把記錄寫到redo log裏面,並更新內存,這個時候更新就算完成了。同時,InnoDB引擎會在適當的時候,將這個操作記錄更新到磁盤裏面,而這個更新往往是在系統比較空閒的時候做。

InnoDB的redo log是固定大小的,比如可以配置爲1組4個文件,每個文件的大小是1GB,那麼總共就可以記錄4GB的操作。從頭開始記錄,寫到末尾就又回到開頭循環寫,如下圖所示:

write pos是當前記錄的位置,一邊寫一邊後移,寫到第3號文件末尾後就回到0號文件開頭。checkpoint是當前要擦除的位置,也是往後推移並且循環的,擦除記錄前要把記錄更新到數據文件。

writepos和checkpoint之間的是空閒部分,可以用來記錄新的操作。如果writepos追上了checkpoint,表示redo log已經滿了,這時候不能再進行新的更新,得先擦除一些記錄,把checkpoint推進一下。

有了redo log,InnoDB就可以保證即使數據庫發生異常重啓,之前提交的記錄也不會丟失,這個能力稱爲crash-safe。

2.2 binlog

前面我們說過,MySQL整體來看,就是就有兩塊:一塊是Server層,主要做MySQL功能層面的事情;還有一塊是引擎層,負責存儲相關的具體事宜。redo log是InnoDB引擎特有的日誌,而Server層也有自己的日誌,稱爲binlog。

這兩種日誌主要有以下三點區別:

  • redo log是InnoDB引擎特有的;binlog是MySQL的Server層實現的,所有引擎都可以使用。
  • redo log是物理日誌,記錄的是“某個數據頁上做了什麼修改”;binlog是邏輯日誌,記錄的是這個語句的原始邏輯,比如“給ID=2這一行的c字段加1”
  • redo log是循環寫的,空間固定會用完;binlog是可以追加寫入的。“追加寫”是指binlog文件寫到一定大小後會切換到下一個,並不會覆蓋以前的日誌。

有了對這兩個日誌概念的理解,我們再來看執行器和InnoDB引擎在執行這個簡單的update語句時的內部流程:

  1. 執行器先找引擎取ID=2這一行。ID是主鍵,引擎直接用樹搜索找到這一行。如果ID=2這一行所在的數據頁本來就在內存中,就直接返回給執行器;否則,需要先從磁盤讀入內存,然後再返回。
  2. 執行器拿到引擎給的行數據,把這個值加上1,比如原來的值是N,現在就是N+1,得到新的一行數據,再調用引擎接口寫入這行數據。
  3. 引擎將這行新數據更新到內存中,同時將這個更新操作記錄到redo log中,此時redo log處於prepare狀態,然後告訴執行器執行完成了,隨時可以提交事務。
  4. 執行器生成這個操作的binlog,並把binlog寫入磁盤。
  5. 執行器調用引擎的提交事務接口,引擎把剛剛寫入的redo log改成提交(commit)狀態,更新完成。

執行流程示意圖,如下圖所示:

MySQL將redo log的寫入拆分成了兩個步驟:prepare和commit,這就是“兩階段提交”。

2.3 兩階段提交

爲什麼必須有“兩階段提交”呢?是爲了讓兩份日誌之間的邏輯一致。要解釋這個問題,我們先解釋一下如何讓數據庫恢復到半個月任意一秒的狀態。

如果DBA承諾說半個月內可以恢復任意一秒的狀態,那麼備份系統中一定會保存最近半個月的所有binlog,同時系統會定期做整庫備份。定期時間可以是一天一備,也可以是一週一備。當需要恢復到指定的某一秒時,我們可以這麼做:

  • 首先,找到最近的一次全量備份,從這個備份恢復到臨時庫。
  • 然後,從備份的時間點開始,將備份的binlog依次取出來,重放到誤刪表之前的那個時刻。
  • 最後將表數據從臨時庫中取出來,按需恢復到線上庫中。

這樣臨時表就跟誤刪之前的線上庫一樣了,然後就可以把表數據從臨時庫中取出來,恢復到線上庫中了。

聊完數據恢復過程,我們來談談爲什麼需要“兩階段提交”。由於redo log和binlog是兩個獨立的邏輯,如果不用兩階段提交,要麼就是先寫完redo log再寫binlog,或者採用相反的順序。我們來看看這兩種方式有什麼問題:

  • 先寫redo log後寫binlog。假設redo log寫完,binlog還沒寫完的時候,MySQL進程異常重啓。redo log寫完之後,系統即使崩潰,仍然能夠把數據恢復回來,但是由於binlog沒有寫完,這時候binlog裏面就沒有這條記錄。如果需要用這個binlog來恢復臨時庫的話,這個臨時庫就會缺少這一次更新。
  • 先寫binlog後寫redo log。如果在binlog寫完之後crash,由於redo log還沒寫,崩潰恢復之後這個事務無效。但是binlog裏面已經記錄了,在之後用binlog來恢復的時候就多了一個事務出來,恢復出來的數據就會與原庫的值不同。

可以看到,如果不使用“兩階段提交”,那麼數據庫的狀態就有可能和用它的日誌恢復出來的庫的狀態不一致。其實,不只是誤操作後需要用這個過程來恢復數據,當需要擴容的時候,也就是需要再多搭建一些備庫來增加系統的讀能力的時候,目前最常見的解決方案是用全量備份加上應用的binlog來實現的,這個“不一致”就會導致線上出現主從數據庫不一致的情況。

最後,總結一下兩階段提交的過程:

prepare->寫binlog->commit

當在寫binlog-之前崩潰時,恢復後發現沒有commit,因此回滾。當在commit之前崩潰時,恢復後雖然沒有commit,但滿足prepare和binlog完整,所以會自動commit。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章