MySQL解析過程、執行過程


轉載:https://student-lp.iteye.com/blog/2152601

https://www.cnblogs.com/cdf-opensource-007/p/6502556.html

說說一條查詢sql的執行流程和底層原理?

執行SQL更新時,其底層經歷了哪些操作?

MySQL解析過程

  • 客戶端發送一條查詢給服務器;
  • 服務器先檢查查詢緩存,如果命中了緩存,則立刻返回存儲在緩存中的結果。否則進入下一階段。
  • 服務器段進行SQL解析、預處理,在優化器生成對應的執行計划;
  • mysql根據優化器生成的執行計划,調用存儲引擎的API來執行查詢。
  • 將結果返回給客戶端。

實際上mysql執行的每一步都比較復雜,具體的過程如下:

1、mysql客戶端和服務器通訊

    mysql客戶端和服務器之間的通訊協議是“半雙工”的,這意味着,在任何一個時刻,要么由服務器向客戶端發送數據,要么由客戶端向服務器發送數據,這兩個動作不能同時發生。這種協議讓mysql通信簡單快速,但也限制了mysql。一個明顯的限制是,這意味着沒辦法進行流量限制。一旦一端開始發生消息,另一端要接收完整個消息才能響應他。

    客戶端用一個單獨的數據包將查詢傳給服務器。一旦客戶端發送了請求,他能做的事情就只是等待結果了。

    相反的,一般服務器響應給用戶的數據通常很多,由多個數據包組成。當服務器開始響應客戶端請求時,客戶端必須完整的接受整個返回結果,而不是簡單的只收取前面幾條結果,然后讓服務器停止發送數據。

    多數連接mysql的庫函數都可以獲得全部結果並緩存到內存里,還可以逐行獲取所需要的數據。默認一般是獲得全部結果並緩存到內存中。mysql通常需要等所有的數據都已經發送給客戶端才能釋放這條查詢所占用的資源,所以接受全部結果並緩存通常可以減少服務器的壓力,讓查詢能夠早點結束、早點釋放對應的資源。

2、  查詢狀態

    對於mysql連接,任何時刻都有一個狀態,該狀態表示了mysql當前正在做什么。使用show full processlist命令查看當前狀態。在一個查詢生命周期中,狀態會變化很多次,下面是這些狀態的解釋:

  • sleep:線程正在等待客戶端發送新的請求;
  • query:線程正在執行查詢或者正在將結果發送給客戶端;
  • locked:在mysql服務器層,該線程正在等待表鎖。在存儲引擎級別實現的鎖,例如InnoDB的行鎖,並不會體現在線程狀態中。對於MyISAM來說這是一個比較典型的狀態。
  • analyzing and statistics:線程正在收集存儲引擎的統計信息,並生成查詢的執行計划;
  • copying to tmp table:線程在執行查詢,並且將其結果集復制到一個臨時表中,這種狀態一般要么是做group by操作,要么是文件排序操作,或者union操作。如果這個狀態后面還有on disk標記,那表示mysql正在將一個內存臨時表放到磁盤上。
  • sorting Result:線程正在對結果集進行排序。
  • sending data:線程可能在多個狀態間傳送數據,或者在生成結果集,或者在想客戶端返回數據。

3、查詢緩存

    在解析一個查詢語句之前,如果查詢緩存是打開的,那么mysql會優先檢查這個查詢是否命中查詢緩存中的數據。這個檢查是通過一個對大小寫敏感的哈希查找實現的。查詢和緩存中的查詢即使只有一個字節不同,那也不會匹配緩存結果,這種情況下查詢就會進入下一階段的處理。

    如果當前的查詢恰好命中了查詢緩存,那么在返回查詢結果之前mysql會檢查一次用戶權限。這仍然是無須解析查詢SQL語句的,因為在查詢緩存中已經存放了當前 查詢需要訪問的表信息。如果權限沒有問題,mysql會跳過所有其他階段,直接從緩存中拿到結果並返回給客戶端。這種情況下,查詢不會被解析,不用生成執行計划,不會被執行。

但是緩存不好用啊。因為:

  • 只要有對一個表的更新,這個表上所有的查詢緩存都會被清空
  • SQL任何字符上的不同,如空格,注釋,都會導致緩存不命中

因此,我能想到用查詢緩存的表,只有一種情況,那就是配置表。其他的業務表,根本是無法利用查詢緩存的特性,或許Mysql團隊也是覺得查詢緩存的使用場景過於局限,就無情的將它剔除。

4、解析器

  將請求的sql生存一顆語法樹。如:

  select username from userinfo

  先通過詞法分析:

從左到右一個字符、一個字符地輸入,然后根據構詞規則識別單詞。你將會生成4個Token,如下所示。"

     面試官:說說一條查詢sql的執行流程和底層原理?
  接下來,進行 語法解析,判斷輸入的這個 SQL 語句是否滿足 MySQL 語法。然后生成下面這樣一顆語法樹:

  如果語法不對,會報錯。

5、處理器

根據一些mysql規則進一步檢查解析樹是否合法。如檢查查詢的表名、列名是否正確,是否有表的權限等。

6、優化器

  當語法樹被認為是合法的了,並且由優化器將其轉化成執行計划。一條查詢可以有很多種執行方式,最后都返回相同的結果。優化器的作用就是找到這其中最好的執行計划。

  執行計划:mysql不會生成查詢字節碼來執行查詢,mysql生成查詢的一棵指令樹,然后通過存儲引擎執行完成這棵指令樹並返回結果。最終的執行計划包含了重構查詢的全部信息。

 

      查詢的生命周期的下一步是將一個SQL轉換成一個執行計划,mysql在依照這個執行計划和存儲引擎進行交互。這包含多個子階段:解析SQL、預處理、優化SQL執行計划。這個過程中任何錯誤都可能終止查詢。

  • 語法解析器和預處理:首先mysql通過關鍵字將SQL語句進行解析,並生成一顆對應的“解析樹”。mysql解析器將使用mysql語法規則驗證和解析查詢;預處理器則根據一些mysql規則進一步檢查解析數是否合法。
  • 查詢優化器:當語法樹被認為是合法的了,並且由優化器將其轉化成執行計划。一條查詢可以有很多種執行方式,最后都返回相同的結果。優化器的作用就是找到這其中最好的執行計划。
  • 執行計划:mysql不會生成查詢字節碼來執行查詢,mysql生成查詢的一棵指令樹,然后通過存儲引擎執行完成這棵指令樹並返回結果。最終的執行計划包含了重構查詢的全部信息。

7、查詢執行引擎

    在解析和優化階段,mysql將生成查詢對應的執行計划,mysql的查詢執行引擎則根據這個執行計划來完成整個查詢。這里執行計划是一個數據結構,而不是和很多其他的關系型數據庫那樣對應的字節碼。

    mysql簡單的根據執行計划給出的指令逐步執行。在根據執行計划逐步執行的過程中,有大量的操作需要通過調用存儲引擎實現的接口來完成。為了執行查詢,mysql只需要重復執行計划中的各個操作,直到完成所有的數據查詢。

8、返回結果給客戶端

    查詢執行的最后一個階段是將結果返回給客戶端。即使查詢不需要返回結果給客戶端,mysql仍然會返回這個查詢的一些信息,如該查詢影響到的行數。如果查詢可以被緩存,那么mysql在這個階段也會將結果放到查詢緩存中。

    mysql將結果集返回客戶端是一個增量、逐步返回的過程。這樣有兩個好處:服務器端無須存儲太多的結果,也就不會因為返回太多結果而消耗太多的內存;這樣處理也讓msyql客戶端第一時間獲得返回的結果。

    結果集中的每一行都會以一個滿足mysql客戶端/服務器通信協議的包發送,再通過tcp協議進行傳輸,在tcp傳輸的過程中,可能對mysql的封包進行緩存然后批量傳輸。

9、讀 寫過程的異同

讀的過程,大致就是上面的流程。寫的話,也會走上面的流程:連接、緩存、解析器、處理器、優化器、執行器。

不同的是,寫會把緩存清空。接下來,解析器會通過詞法和語法解析知道這是一條更新語句。優化器決定要使用 哪個索引。然后,執行器負責具體執行,找到這一行,然后更新。

與讀流程不一樣的是,寫流程還涉及兩個重要的日志模塊,它們正是:redo log(重做日志)和 binlog(歸檔日志)。

------執行器和 InnoDB 引擎在執行update語句時的內部流程

例如: update T set c=c+1 where ID=2;

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

 

 最后三步看上去有點“繞”,將 redo log 的寫入拆成了兩個步驟:prepare 和 commit,這就是"兩階段提交"。

為什么必須有“兩階段提交”呢?這是為了讓兩份日志之間的邏輯一致。

MySQL執行過程

SQL是一套標准,全稱結構化查詢語言,是用來完成和數據庫之間的通信的編程語言,SQL語言是腳本語言,直接運行在數據庫上。同時,SQL語句與數據在數據庫上的存儲方式無關,只是不同的數據庫對於同一條SQL語句的底層實現不同罷了,但結果相同。這有點類似於java中接口的作用,一個接口可以有不同的實現類,不同的實現類對於接口中方法的實現方式可以不同,結果可以相同。這里SQL語言的作用就類似於java中的接口,數據庫就類似於java中接口的實現類,SQL語句就類似於java接口中的方法。不同的是java中接口的不同實現類對於接口中方法的執行結果可以相同,也可以不同,而不同的數據庫對於同一條SQL語句的執行是相同的。(這里只是做一個類比,方便我們理解)

  一般情況下,大部分SQL語句在不同的數據庫上是通用的,但我們知道每個數據庫都有自己獨有的特性,像在MySql數據庫中,可以使用substr(取字符串),trim(去空格),ifnull(空值處理函數),還可以使用limit語句對數據庫表進行截取,但這些都是oracle數據庫沒有的。(類比接口實現類中,實現類獨有的方法,而接口中沒有的)

  這里簡單介紹一下mysql數據庫,mysql數據庫是一款關系型數據庫,所謂關系型數據庫就是以二維表的形式存儲數據,使用行和列方便我們對數據的增刪改查。

  這篇博客,我們以mysql數據庫為例,對一條sql語句的執行流程進行分析。(本篇博客不涉及到表連接)

  首先,創建一張student表,字段有自增主鍵id,學生姓名name,學科subject,成績grade

  建表語句:

復制代碼
DROP TABLE IF EXISTS student;
CREATE TABLE `student` (
  `id` int(5) NOT NULL AUTO_INCREMENT,
  `name` varchar(10) DEFAULT NULL,
  `subject` varchar(10) DEFAULT NULL,
  `grade` double(4,1) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=40 DEFAULT CHARSET=utf8;
復制代碼

  初始化數據:

復制代碼
INSERT INTO student(`name`,`subject`,grade)VALUES('aom','語文',88);
INSERT INTO student(`name`,`subject`,grade)VALUES('aom','數學',99);
INSERT INTO student(`name`,`subject`,grade)VALUES('aom','外語',55);

INSERT INTO student(`name`,`subject`,grade)VALUES('jack','語文',67);
INSERT INTO student(`name`,`subject`,grade)VALUES('jack','數學',44);
INSERT INTO student(`name`,`subject`,grade)VALUES('jack','外語',55);

INSERT INTO student(`name`,`subject`,grade)VALUES('susan','語文',56);
INSERT INTO student(`name`,`subject`,grade)VALUES('susan','數學',35);
INSERT INTO student(`name`,`subject`,grade)VALUES('susan','外語',77);

INSERT INTO student(`name`,`subject`,grade)VALUES('alice','語文',88);
INSERT INTO student(`name`,`subject`,grade)VALUES('alice','數學',77);
INSERT INTO student(`name`,`subject`,grade)VALUES('alice','外語',100);

INSERT INTO student(`name`,`subject`,grade)VALUES('rajo','語文',33);
INSERT INTO student(`name`,`subject`,grade)VALUES('rajo','數學',55);
INSERT INTO student(`name`,`subject`,grade)VALUES('rajo','外語',55);
復制代碼

下面我們來看一下,數據在數據庫中的存儲形式。

  (圖1.0)

現在針對這張student表中的數據提出一個問題:要求查詢出掛科數目多於兩門(包含兩門)的前兩名學生的姓名,如果掛科數目相同按學生姓名升序排列。

下面是這條查詢的sql語句

SELECT `name`,COUNT(`name`) AS num FROM student WHERE grade < 60 GROUP BY `name` HAVING num >= 2 ORDER BY num DESC,`name` ASC LIMIT 0,2;

執行結果:

  圖(1.1)

以上這條sql語句基本上概括了單表查詢中所有要注意的點,那么我們就以這條sql為例來分析一下一條語句的執行流程。

1,一條查詢的sql語句先執行的是 FROM student 負責把數據庫的表文件加載到內存中去

如圖1.0中所示。(mysql數據庫在計算機上也是一個進程,cpu會給該進程分配一塊內存空間,在計算機‘服務’中可以看到,該進程的狀態)

  圖(1.2)

2,WHERE grade < 60

會把(圖1.0)所示表中的數據進行過濾,取出符合條件的記錄行,生成一張臨時表,如下圖所示。

  圖(1.3)

3,GROUP BY `name`

會把圖(1.3)的臨時表切分成若干臨時表,我們用下圖來表示內存中這個切分的過程。

                

  圖(1.4)              圖(1.5)               圖(1.6)              圖(1.7)

4,SELECT 的執行讀取規則分為sql語句中有無GROUP BY兩種情況。

  (1)當沒有GROUP BY時,SELECT 會根據后面的字段名稱對內存中的一張臨時表整列讀取。

  (2)當查詢sql中有GROUP BY時,會對內存中的若干臨時表分別執行SELECT,而且只取各臨時表中的第一條記錄,然后再形成新的臨時表。這就決定了查詢sql使用GROUP BY的場景下,SELECT后面跟的一般是參與分組的字段和聚合函數,否則查詢出的數據要是情況而定。另外聚合函數中的字段可以是表中的任意字段,需要注意的是聚合函數會自動忽略空值。

  我們還是以本例中的查詢sql來分析,現在內存中有四張被GROUP BY `name`切分成的臨時表,我們分別取名為 tempTable1,tempTable2,tempTable3,tempTable4分別對應圖(1.4)、圖(1.5)、圖(1.6),圖(1.7)下面寫四條"偽SQL"來說明這個查詢過程。

SELECT `name`,COUNT(`name`) AS num FROM tempTable1;
SELECT `name`,COUNT(`name`) AS num FROM tempTable2;
SELECT `name`,COUNT(`name`) AS num FROM tempTable3;
SELECT `name`,COUNT(`name`) AS num FROM tempTable4;

最后再次成新的臨時表,如下圖:

  圖(1.8)

5,HAVING num >= 2對上圖所示臨時表中的數據再次過濾

與WHERE語句不同的是HAVING 用在GROUP BY之后,WHERE是對FROM student從數據庫表文件加載到內存中的原生數據過濾,而HAVING 是對SELECT 語句執行之后的臨時表中的數據過濾,所以說column AS otherName ,otherName這樣的字段在WHERE后不能使用,但在HAVING 后可以使用。但HAVING的后使用的字段只能是SELECT 后的字段,SELECT后沒有的字段HAVING之后不能使用。HAVING num >= 2語句執行之后生成一張臨時表,如下:

   圖(1.9)

6,ORDER BY num DESC

`name` ASC對以上的臨時表按照num,name進行排序。

7,LIMIT 0,2

取排序后的前兩個。

以上就是一條sql的執行過程,同時我們在書寫查詢sql的時候應當遵守以下順序。

SELECT XXX FROM XXX WHERE XXX GROUP BY XXX HAVING XXX ORDER BY XXX LIMIT XXX;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM