【MySQL系列6】詳解一條查詢select語句和一條更新update語句的執行流程

前言

本文基於MySQL5.7版本。

前面幾篇MySQL系列的文章介紹了索引,事務和鎖相關知識,那麼今天就讓我們來看看當我們執行一條select語句和一條update語句的時候,MySQL要經過哪些步驟,才能返回我們想要的數據。

一條select語句的執行流程

MySQL從大方向來說,可以分爲 Server 層和存儲引擎層。而Server層包括連接器、查詢緩存、解析器、預處理器、優化器、執行器等,最後Server層再通過API接口形式調用對應的存儲引擎層提供的接口。如下圖所示(圖片來源於《高性能MySQL》):
在這裏插入圖片描述
根據流程圖,一條select查詢大致經過以下六個步驟:
1、客戶端發起一個請求時,首先會建立一個連接
2、服務端會檢查緩存,如果命中則直接返回,否則繼續之後後面步驟
3、服務器端根據收到的sql語句進行解析,然後對其進行詞法分析,語法分析以及預處理
4、由優化器生成執行計劃
5、調用存儲引擎層API來執行查詢
6、返回查詢到的結果

查詢流程也可以通過如下圖表示(圖片來源於丁奇MySQL45講):
在這裏插入圖片描述

建立連接

第一步建立連接,這個很容易理解,需要特別指出的是MySQL服務端和客戶端的通信方式採用的是半雙工協議

通信方式主要可以分爲三種:單工,半雙工,全雙工,如下圖:
在這裏插入圖片描述

  • 單工:通信的時候,數據只能單向傳輸。比如說遙控器,我們只能用遙控器來控制電視機,而不能用電視機來控制遙控器。
  • 半雙工:通信的時候,數據可以雙向傳輸,但是同一時間只能有一臺服務器在發送數據,當A給B發送數據的時候,那麼B就不能給A發送數據,必須等到A發送結束之後,B才能給A發送數據。比如說對講機。
  • 全雙工:通信的時候,數據可以雙向傳輸,並且可以同時傳輸。比如說我們打電話或者用通信軟件進行語音和視頻通話等。

半雙工協議讓MySQL通信簡單快速,但是也在一定程度上限制了MySQL的性能,因爲一旦從一端開始發送數據,另一端必須要接收完全部數據才能做出響應。所以說我們批量插入的時候儘量拆分成多次插入而不要一次插入太大數據,同樣的查詢語句最好也帶上limit限制條數,避免一次返回過多數據。

MySQL單次傳輸數據包的大小可以通過參數max_allowed_packet控制,默認大小爲4MB

SHOW VARIABLES LIKE 'max_allowed_packet';

在這裏插入圖片描述

查詢緩存

連接上了之後,如果緩存是打開的,那麼就會進入查詢緩存階段,可以通過如下命令查看緩存是否開啓:

SHOW VARIABLES LIKE 'query_cache_type';

在這裏插入圖片描述
我們可以看到,緩存默認是關閉的。這是因爲MySQL的緩存使用條件非常苛刻,是通過一個大小寫敏感的哈希值去匹配的,這樣就是說一條查詢語句哪怕只是有一個空格不一致,都會導致無法使用緩存。而且一旦表裏面有一行數據變動了,那麼關於這種表的所有緩存都會失效。所以一般我們都是不建議使用緩存,MySQL最新的8.0版本已經將緩存模塊去掉了。

解析器和預處理器

跳過了緩存模塊之後,查詢語句會進入解析器進行解析。

詞法解析和語法解析(Parser)

這一步主要的工作就是檢查sql語句的語法對不對,在這裏,首先會把我們整個SQL語句打碎,比如:select name from test where id=1,就會被打散成select,name,from,test,where,id,=,1 這8個字符,並且能識別出關鍵字和非關鍵字,然後根據sql語句生成一個數據結構,也叫做解析樹(select_lex),如下圖:
在這裏插入圖片描述

預處理器(Preprocessor)

經過了前面的詞法和語法解析,那麼至少我們一條sql語句的語法格式是滿足要求了,接下來我們還需要做什麼呢?自然是檢查表名,列名以及其他一些信息等是不是真實存在的,預處理就是做一個表名和字段名等相關信息合法性的檢測

查詢優化器(Query Optimizer)

經過上面的步驟,到這裏就得到了一句有效的sql語句了。而對一個查詢語句,尤其是複雜的多表查詢語句,我們可以有很多種執行方式,每種執行方式的效率也不一樣,所以這時候就需要查詢優化器去選擇一種它認爲最高效的執行方式。

查詢優化器的目的就是根據解析樹生成不同的執行計劃(Execution Plan),然後選擇一種最優的執行計劃,MySQL 裏面使用的是基於開銷(cost)的優化器,哪種執行計劃開銷最小,就選擇哪種。

我們可以通過變量Last_query_cost來查詢開銷:

SELECT * FROM test;
show status like 'Last_query_cost';

在這裏插入圖片描述
上圖中展示的結果就表示MySQL認爲SELECT * FROM test 查詢語句需要做至少2個數據頁的隨機查找才能完成上面的查詢。
這個結果是通過一系列複雜的運算得到的,包括每個表或者索引的頁面個數,索引的基數,索引和數據行的長度,索引分佈的情況。

優化器在評估成本的時候,不會考慮任何緩存的作用,而是假設讀取任何數據都需要經過一次IO操作。

優化器可以做哪些優化

優化器可以替我們做很多優化,下面列舉一些常用的優化:

  • 重新定義關聯的順序。優化器並不一定按照我們寫的查詢關聯語句中的關聯順序,而是會按照優化後的順序進行查詢。
  • 將外連接轉爲爲內連接。
  • 使用等價轉換原則。比如a<b and a=5會被轉換爲a=5 and b>5
  • 優化COUNT(),MIN()和MAX()
  • 預估並轉化爲常數表達式
  • 覆蓋索引掃描。想要詳細瞭解覆蓋索引的可以點擊這裏
  • 子查詢優化。
  • 提前終止查詢。比如我們使用了一個不成立的條件,則會立刻返回空。
  • 等值傳播。
  • 優化IN()語句。在其他很多數據庫中in等同於or語句,但是MySQL中會講in中的值先進行排序,然後按照二分查找的方法來確定是否滿足條件。

實際當中優化器能做的優化遠遠比上面列舉的更多,所以有時候我們不要覺得比優化器更聰明,所以大部分情況下我們都可以讓優化器做出優化就可以了,如果有些我們確定優化器沒有選擇最優的查詢方案,我們也可以在查詢中通過添加hint提示告知到優化器,比如通過force index強制使用索引或者straight_join語句強制優化器按我們想要的表順序進行關聯。

優化器並不是萬能的

MySQL優化器也並不是萬能的,並不是總能把我們寫的糟糕的sql語句優化成一個高效的查詢語句,而且也有很多種原因會導致優化器做出錯誤的選擇:

  • 統計信息不準確。MySQL評估成本依賴於存儲引擎提供的的統計信息,然而存儲引擎提供的統計信息有時候會有較大偏差。
  • 執行計劃的成本估算不等於實際的執行成本。比如估算成本的時候不考慮緩存,而實際執行有些數據在緩存中。
  • 優化器認爲的最優可能並不是我們需要的最優。比如有時候我們想要時間最短,但是優化器
  • 優化器從不考慮其他併發的查詢。
  • 優化器並不總是基本成本的優化。有時候也會基於規則,比如當存在全文索引,查詢時使用了match()子句時,即使選擇其他索引更優,優化器仍然會選擇全文索引。
  • 優化器不將不受其控制的操作計算爲成本。如執行存儲過程或者用戶自定義函數的成本。
  • 優化器有時候無法估算所有的執行計劃,所以也有可能錯過最優執行計劃。

優化器如何得到查詢計劃

優化器聽起來比較抽象,給人一種看不見摸不着的感覺,但是實際上我們也可以通過參數打開優化器追蹤,優化器追蹤默認是關閉的,因爲開啓後會影響性能,所以建議是在需要定位問題的時候開啓,並及時關閉。

SHOW VARIABLES LIKE 'optimizer_trace';
set optimizer_trace='enabled=on';

接下來執行一句查詢語句:

SELECT t1.name AS name1,t2.name AS name2 FROM test t1 INNER JOIN test2 t2 ON t1.id=t2.id

這時候優化器的分析過程已經被記錄下來了,可以通過下面語句查詢:

SELECT * FROM information_schema.optimizer_trace;

得到如下結果:
在這裏插入圖片描述
上面的圖是爲了看數據效果,如果需要自己操作的話,需要用shelll命令窗口去執行,sqlyog工具中直接查詢出來TRACE列是空的,shell中返回的TRACE列信息如下:
在這裏插入圖片描述
從截圖中的輪廓可以看出來這是一個json數據格式。

跟蹤信息主要分爲以下三部分(上圖並未將全部內容展示出來,感興趣的可以自己去嘗試一下,開啓之後記得及時關閉哦):

  • 準備階段(join_preparation):expanded_query中的查詢語句就是優化後的sql
  • 優化階段(join_optimization):considered_execution_plans中列出來所有的執行計劃
  • 執行階段(join_execution)

considered_execution_plans中的執行計劃我們也可以通過explain關鍵字去查看,想要詳細瞭解explain的可以點擊這裏

存儲引擎查詢

當Server層得到了一條sql語句的執行計劃後,這時候就會去調用存儲引擎層對應的API,執行查詢了。因爲MySQL的存儲引擎是插件式的,所以每種存儲引擎都會對Server提供了一些對應的API調用。
想要了解MySQL存儲引擎相關知識的,可以點擊這裏

返回結果

最後,將查詢出得到的結果返回Server層,如果開啓了緩存,Server層返回數據的同時還會寫入緩存。

MySQL將查詢結果返回是一個增量的逐步返回過程。例如:當我們處理完所有查詢邏輯並開始執行查詢並且生成第一條結果數據的時候,MySQL就可以開始逐步的向客戶端傳輸數據了。這麼做的好處是服務端無需存儲太多結果,從而較少內存消耗(這個操作可以通過sql _buffer_result來提示優化器,和上文說的force index,straight_join一樣都是人爲強制優化器執行我們想要的操作)。

一條update語句的執行流程

一條更新語句,其實是增,刪,查的綜合體,查詢語句需要經過的流程,更新語句全部需要執行一次,因爲更新之前必須要先拿到(查詢)需要更新的數據。

Buffer Pool

InnnoDB的數據都是放在磁盤上的,而磁盤的速度和CPU的速度之間有難以逾越的鴻溝,爲了提升效率,就引入了緩衝池技術,在InnoDB中稱之爲Buffer Pool。

從磁盤中讀取數據的時候,會先將從磁盤中讀取到的頁放在緩衝池中,這樣下次讀相同的頁的時候,就可以直接從Buffer Pool中獲取。

更新數據的時候首先會看數據在不在緩衝池中,在的話就直接修改緩衝池中的數據,注意,前提是我們不需要對這條數據進行唯一性檢查(因爲如果要進行唯一性檢查就必須加載磁盤中的數據來判斷是否唯一了)

如果只修改了Buffer Pool中的數據而不修改磁盤中數據,這時候就會造成內存和磁盤中數據不一致,這種也叫做髒頁。InnoDB 裏面有專門的後臺線程把 Buffer Pool 的數據寫入到磁盤, 每隔一段時間就一次性地把多個修改寫入磁盤,這個動作就叫做刷髒。

那麼現在有一個問題,假如我們更新都需要把數據寫入數據磁盤,然後磁盤也要找到對應的那條記錄,然後再更新,整個過程 IO 成本、查找成本都很高。爲了解決這個問題,InnoDB就有了redo log,並且採用了Write-Ahead Logging(WAL)方案實現。

redo log

redo log,即重做日誌,是InnoDB引擎所特有,主要用於崩潰修復(crash-safe)。

Write-Ahead Logging(WAL)

Write-Ahead Logging,即先寫日誌,也就是說我們執行一個操作的時候會先將操作寫入日誌,然後再寫入數據磁盤,那麼有人就會問了,寫入數據表是磁盤操作,寫入redo log也是磁盤操作,同樣都是寫入磁盤,爲什麼不直接寫入數據,而要先寫入日誌呢?這不是多次一舉嗎?

設想一下,假如我們所需要的數據是隨機分散在不同頁的不同扇區中,那麼我們去找數據的時候就是隨機IO操作,而redo log是循環寫入的,也就是順序IO。一句話:
刷盤是隨機 I/O,而記錄日誌是順序 I/O,順序 I/O 效率更高。因此先把修改寫入日 志,可以延遲刷盤時機,進而提升系統吞吐

redo log是如何刷盤的

InnoDB中的 redo log是固定大小的,也就是說redo log並不是隨着文件寫入慢慢變大,而是一開始就分配好了空間,空間一旦寫滿了,前面的空間就會被覆蓋掉,刷盤的操作是通過Checkpoint實現的。如下圖:
在這裏插入圖片描述
check point 是當前要覆蓋的位置。write pos是當前寫入日誌的位置。寫日誌的時候是循環寫的,覆蓋舊記錄前要把記錄更新到數據文件。如果write pos和 check point 重疊,說明redo log 已經寫滿,這時候需要同步redo log刷到磁盤中。

bin log

MySQL整體來看,其實就有兩塊:一塊是 Server 層,它主要做的是 MySQL功能層面的事情;還有一塊是引擎層,負責存儲相關的具體事宜。上面講的redo log是InnoDB 引擎特有的日誌,而Server 層也有自己的日誌,稱爲 binlog(歸檔日誌),也叫做二進制日誌。

可能有人會問,爲什麼會有兩份日誌呢?
因爲最開始 MySQL 裏並沒有 InnoDB 引擎。MySQL 自帶的引擎是 MyISAM,但是 MyISAM是不支持事物的,也沒有崩潰恢復(crash-safe)的能力,binlog日誌只能用於歸檔。那麼既然InnoDB是需要支持事務的,那麼就必須要有崩潰恢復(crash-safe)能力,所以就使用另外一套自己的日誌系統,也就是基於redo log 來實現 crash-safe 能力。

bin log和redo log的區別

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

update語句的執行流程

前面鋪墊了這麼多,主要是想讓大家先理解redo log和big log這兩個概念,因爲更新操作離不開這兩個文件,接下來我們正式回到正題,一條update語句到底是如何執行的,可以通過下圖表示:
在這裏插入圖片描述
上圖可以大概概括爲以下幾步:
1、先根據更新語句的條件,查詢出對應的記錄,如果有緩存,也會用到緩存
2、Server端調用InnoDB引擎API接口,InnoDB引擎將這條數據寫到內存,同時寫入redo log,並將redo log狀態設置爲prepare
3、通知Server層,可以正式提交數據了
4、Server層收到通知後立刻寫入bin log,然後調用InnoD對應接口發出commit請求
5、InnoDB收到commit請求後將數據設置爲commit狀態

上面的步驟中,我們注意到,redo log會經過兩次提交,這就是兩階段提交。

兩階段提交

兩階段提交是分佈式事務的設計思想,就是首先會有請求方發出請求到各個服務器,然後等其他各個服務器都準備好之後再通知請求方可以提交了,請求方收到請求後再發出指令,通知所有服務器一起提交。

而我們這裏redo log是屬於存儲引擎層的日誌,bin log是屬於Server層日誌,屬於兩個獨立的日誌文件,採用兩階段提交就是爲了使兩個日誌文件邏輯上保持一致

假如不採用兩階段提交法

假如有一條語句id=1,age=18,我們現在要把這條數據的age更新爲19:

  • 先寫 redo log 後寫 binlog
    假設在redo log 寫完,binlog還沒有寫完的時候,MySQL發生了宕機(crash)。重啓後因爲redo log寫完了,所以會自動進行數據恢復,也就是age=19。但是由於binlog沒寫完就宕機( crash)了,這時候 binlog 裏面就沒有記錄這個語句。因此,之後備份日誌的時候,存起來的 binlog 裏面就沒有這條語句。然後某一天假如我們把數據丟失了,需要用bin log進行數據恢復就會發現少了這一次更新。
  • 先寫binlog後寫redo log
    假如在binlog寫完,redo log還沒有寫完的時候,MySQL發生了宕機(crash)。重啓後因爲redo log沒寫完,所以無法進行自動恢復,那麼數據就還是age=18了,然後某一天假如我們把數據丟失了,需要用binlog進行恢復又會發現恢復出來的數據age=19了。

通過以上的兩個假設我們就會發現,假如不採用兩階段提交法就會出現數據不一致的情況,尤其是在有主從庫的時候,因爲主從複製是基於binlog實現的,如果redo log和bin log不一致,就會導致主從庫數據不一致。

宕機後的數據恢復規則

1、如果 redo log 裏面的事務是完整的,也就是已經有了 commit 標識,則直接提交;
2、如果 redo log 裏面的事務只有完整的 prepare,則判斷對應的事務 binlog 是否存在並完整:如果是,則提交事務;否則,回滾事務。

總結

本文主要分析了select和update語句的執行過程,而在分析update語句執行過程中,又簡單介紹了redo log和bin log相關概念,這一部分內容在本文中沒有過多深入的講解,僅僅只是爲了讓大家去理解更新流程而做了簡單的介紹,像redo log和其對應的緩存之間的關係,redo log刷盤策略,bin log寫入策略,有了bin log爲何還需要redo log等等問題本文中並沒有給出明確的解釋,因爲本文篇幅有限,深入之後就會涉及到InnoDB引擎的存儲結構以及更底層的一些知識,關於這些內容將在《MySQL系列》的後續文章中都會進行一一介紹。請關注我,和孤狼一起學習

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