【轉】2.1【MySQL】執行原理(一):查詢sql的執行過程及MySQL架構分析
MySQL的發展歷史和版本分支:
時間 | 里程碑 |
---|---|
1996 年 | MySQL1.0 釋出。它的歷史可以追溯到 1979 年,作者 Monty 用 BASIC 設計的一個報表工具。 |
1996 年 10 月 | 3.11.1 釋出。MySQL 沒有 2.x 版本。 |
2000 年 | ISAM 升級成 MyISAM 引擎。MySQL 開源。 |
2003 年 | MySQL4.0 釋出,整合 InnoDB 儲存引擎 |
2005 年 | MySQL5.0 版本釋出,提供了檢視、儲存過程等功能。 |
2008 年 | MySQLAB 公司被 Sun 公司收購,進入 SunMySQL 時代。 |
2009 年 | Oracle 收購 Sun 公司,進入 OracleMySQL 時代。 |
2010 年 | MySQL5.5 釋出,InnoDB 成為預設的儲存引擎。 |
2016 年 | MySQL 釋出 8.0.0 版本。為什麼沒有 6、7?5.6 可以當成 6.x,5.7 可以當 成 7.x。 |
因為MySQL是開源的(也有收費版本),所以在MySQL穩定版本的基礎上也發展出來了很多的分支,就像Linux一樣,有Ubuntu、RedHat、CentOS、 Fedora 、Debian等等。大家最熟悉的應該是MariaDB,因為CentOS7裡面自帶了一個MariaDB。它是怎麼來的呢?Oracle收購MySQL之後,MySQL創始人之一Monty擔心MySQL資料庫的未來(開發緩慢,封閉,可能會被閉源),就建立了一個分支MariaDB,預設使用全新的Maria儲存引擎,它是原MyISAM儲存引擎的升級版本。
其他流行分支:
- Percona Server 是MySQL重要的分支之一,它基於InnoDB儲存引擎的基礎上,提升了效能和易管理性,最後形成了增強版的XtraDB引擎,可以用來更好地發揮伺服器硬體上的效能。
- 國內也有一些MySQL的分支或者自研的儲存引擎,比如網易的InnoSQL,極數雲舟的ArkDB。
我們操作資料庫有各種各樣的方式,比如Linux 系統中的命令列,比如資料庫工具Navicat,比如程式,例如Java語言的JDBC API或者ORM框架。但大家有沒有思考過,當我們的工具或者程式連線到資料庫之後,實際上發生了什麼事情?它的內部是怎麼工作的?
下面以資料查詢為例,來看下MySQL的工作流程是什麼樣的:
我們的程式或者工具要操作資料庫,第一步要做什麼事情?跟資料庫建立連線。
1.與資料庫建立連線
首先,MySQL必須要執行一個服務,監聽預設的3306埠。在我們開發系統跟第三方對接的時候,必須要弄清楚的有兩件事。
- 第一個就是通訊協議,比如我們是用TCP/UDP還是HTTP還是WebService?
- 第二個是訊息格式,比如我們用XML格式,還是JSON格式,還是定長格式?報文頭長度多少,包含什麼內容,每個欄位的詳細含義?
1.1 通訊型別和連線方式
MySQL是支援多種通訊協議的,可以使用同步/非同步的方式,支援長連線/短連線。這裡我們拆分來看。
通訊型別:同步或者非同步
-
同步通訊的特點:
- 同步通訊依賴於被呼叫方,受限於被呼叫方的效能。也就是說,應用操作資料庫,執行緒會阻塞,等待資料庫的返回
- 一般只能做到一對一,很難做到一對多的通訊
-
非同步跟同步相反:
- 非同步可以避免應用阻塞等待,但是不能節省SQL執行的時間
- 如果非同步存在併發,每一個SQL的執行都要單獨建立一個連線,避免資料混亂。但是這樣會給服務端帶來巨大的壓力(一個連線就會建立一個執行緒,執行緒間切換會佔用大量CPU資源)。另外非同步通訊還帶來了編碼的複雜度,所以一般不建議使用。如果要非同步,必須使用連線池,排隊從連線池獲取連線而不是建立新連線
一般來說我們連線資料庫都是同步連線。
連線方式:長連線或者短連線
MySQL既支援短連線,也支援長連線。短連線就是操作完畢以後,馬上close 掉。長連線可以保持開啟,減少服務端建立和釋放連線的消耗,後面的程式訪問的時候還可以使用這個連線。一般我們會在連線池中使用長連線。
保持長連線會消耗記憶體。長時間不活動的連線,MySQL伺服器會斷開。那這個超時時間怎麼檢視呢?
show global variables like 'wait_timeout'; -- 非互動式超時時間,如 JDBC 程式
show global variables like' interactive_timeout'; -- 互動式超時時間,如資料庫工具
執行結果如下圖。預設都是28800秒,8小時
我們怎麼檢視MySQL當前有多少個連線?可以用show status命令:
show global status like 'Thread%';
- Threads_cached:快取中的執行緒連線數
- Threads_connected:當前開啟的連線數
- Threads_created:為處理連線建立的執行緒數
- Threads_running:非睡眠狀態的連線數,通常指併發連線數
有了連線數,怎麼知道當前連線的狀態?可以使用show processlist命令,(root使用者)檢視SQL的執行狀態。
SHOW PROCESSLIST;
從上面的Threads_connected可以看到當前有4個連線,所以這裡的顯示了4個連線狀態。那Command這一列是什麼意思呢?一些常見的狀態:
狀態 | 含義 |
---|---|
Sleep | 執行緒正在等待客戶端,以向它傳送一個新語句 |
Query | 執行緒正在執行查詢或往客戶端傳送資料 |
Locked | 該查詢被其它查詢鎖定 |
Copying to tmp tableondisk | 臨時結果集合大於 tmp_table_size。執行緒把臨時表從儲存器內部格式改 變為磁碟模式,以節約儲存器 |
Sendingdata | 執行緒正在為 SELECT 語句處理行,同時正在向客戶端傳送資料 |
Sortingforgroup | 執行緒正在進行分類,以滿足 GROUPBY 要求 |
Sortingfororder | 執行緒正在進行分類,以滿足 ORDERBY 要求 |
還有個問題,MySQL服務允許的最大連線數是多少呢?
show variables like 'max_connections';
在5.7版本中預設是151個,最大可以設定成16384(2^14)。
set global max_connections=1000;
1.2 MySQL支援哪些通訊協議
第一種是Unix Socket。比如我們在Linux伺服器上,如果沒有指定-h引數,它就用socket方式登入。如下圖(省略
了-S /var/lib/mysql/mysql.sock)
它不用通過網路協議,也可以連線到MySQL的伺服器,它需要用到伺服器上的一個物理檔案(/var/lib/mysql/mysql.sock)
select @@socket;
如果有引數-h指定主機,就會用第二種方式,TCP/IP協議。
mysql -h192.168.8.211 -uroot -p123456
我們的程式語言的連線模組都是用 TCP 協議連線到 MySQL 伺服器的,比如 mysql-connector-java-x.x.xx.jar。
1.3 MySQL採用什麼通訊方式
通訊方式分為以下三種:
- 單工:在兩臺計算機通訊的時候,資料的傳輸是單向的。比如遙控器
- 半雙工:在兩臺計算機之間,資料傳輸是雙向的,你可以給我傳送,我也可以給你傳送,但是在這個通訊連線裡面,同一時間只能有一臺伺服器在傳送資料,也就是你要給我發的話,也必須等我發給你完了之後才能給我發。比如對講機
- 全雙工:資料的傳輸是雙向的,並且可以同時傳輸。比如打電話
那MySQL應該採用哪種通訊方式呢?半雙工的通訊方式,因為客戶端與服務端肯定是雙向通訊的,而且要麼是客戶端向服務端傳送資料,要麼是服務端向客戶端傳送資料,這兩個動作不能同時發生。
客戶端傳送SQL語句給服務端的時候,(在一次連線裡面)資料是不能分成小塊傳送的,不管你的SQL語句有多大,都是一次性發送。比如我們用MyBatis動態SQL生成了一個批量插入的語句, 插入10萬條資料, values後面跟了一長串的內容,或者where條件in裡面的值太多,會出現問題。這個時候我們必須要調整MySQL伺服器配置 max_allowed_packet 引數的值(預設是4M),把它調大,否則就會報錯。
另一方面,對於服務端來說,也是一次性發送所有的資料,不能因為你已經取到了想要的資料就中斷操作,這個時候會對網路和記憶體產生大量消耗。所以,我們一定要在程式裡面避免不帶limit 的這種操作,比如一次把所有滿足條件的資料全部查出來,一定要先count一下。如果資料量大的話,可以分批查詢。
執行一條查詢語句,客戶端跟服務端建立連線之後呢?下一步要做什麼?
2.查詢快取
MySQL內部自帶了一個快取模組。快取的作用我們應該很清楚了,把資料以KV的形式放到記憶體裡面,可以加快資料的讀取速度,也可以減少伺服器處理的時間。但是MySQL的快取我們好像比較陌生,從來沒有去配置過,也不知道它什麼時候生效?假如 t_user 表有500萬行資料,沒有索引。我們在沒有索引的欄位上執行同樣的查詢,大家覺得第二次會快嗎?
可以看到MySQL快取是預設關閉的。預設關閉的意思就是不推薦使用,為什麼MySQL不推薦使用它自帶的快取呢?
主要是因為MySQL自帶的快取的應用場景有限,需要滿足以下兩個要求:
- 它要求SQL語句必須一模一樣,中間多一個空格,字母大小寫不同都被認為是不同的的SQL。
- 表裡面任何一條資料發生變化的時候,這張表所有快取都會失效,所以對於有大量資料更新的應用,也不適合。
所以快取這一塊,我們還是交給ORM框架(比如MyBatis預設開啟了一級快取),或者獨立的快取服務,比如Redis來處理更合適。在MySQL 8.0中,查詢快取已經被移除了。
我們沒有使用快取的話,就會跳過快取的模組,下一步我們要做什麼呢?
3. 語法解析和預處理
這裡我會有一個疑問,為什麼我的一條SQL語句能夠被識別呢?假如我隨便執行一個字串penyuyan,伺服器報了一個1064的錯:
它是怎麼知道我輸入的內容是錯誤的?這個就是MySQL的Parser解析器和Preprocessor預處理模組。這一步主要做的事情是對語句基於SQL語法進行詞法和語法分析和語義的解析。
3.1 詞法分析
詞法分析就是把一個完整的SQL語句打碎成一個個的單詞。比如一個簡單的SQL語句:
select username from t_user where id = 1;
它會打碎成8個符號,每個符號是什麼型別,從哪裡開始到哪裡結束。
3.2 語法分析
第二步就是語法分析,語法分析會對SQL做一些語法檢查,比如單引號有沒有閉合,然後根據MySQL定義的語法規則,根據SQL語句生成一個數據結構。這個資料結構我們把它叫做解析樹(select_lex)。
任何資料庫的中介軟體,比如 Mycat,Sharding-JDBC(用到了Druid Parser),都必須要有詞法和語法分析功能,在市面上也有很多的開源的詞法解析的工具(比如LEX,Yacc)。
3.3 前處理器
問題:如果我寫了一個詞法和語法都正確的SQL,但是表名或者欄位不存在,會在哪裡報錯?是在資料庫的執行層還是解析器?比如:
select * from penyuyan;
解析器可以分析語法,但是它怎麼知道資料庫裡面有什麼表,表裡面有什麼欄位呢?實際上還是在解析的時候報錯,解析SQL的環節裡面有個前處理器。它會檢查生成的解析樹,解決解析器無法解析的語義。比如,它會檢查表和列名是否存在,檢查名字和別名,保證沒有歧義。預處理之後得到一個新的解析樹。
4.查詢優化得到執行計劃
得到解析樹之後,是不是執行SQL語句了呢?這裡我們有一個問題,一條SQL語句是不是隻有一種執行方式?或者說資料庫最終執行的SQL是不是就是我們傳送的SQL?
這個答案是否定的。一條SQL語句是可以有很多種執行方式的,最終返回相同的結果,他們是等價的。但是如果有這麼多種執行方式,這些執行方式怎麼得到的?最終選擇哪一種去執行?根據什麼判斷標準去選擇?
這個就是MySQL的查詢優化器的模組(Query Optimizer)。
4.1 什麼是優化器?
查詢優化器的目的就是根據解析樹生成不同的執行計劃(ExecutionPlan),然後選擇一種最優的執行計劃。MySQL裡面使用的是基於開銷(cost)的優化器,哪種執行計劃開銷最小,就用哪種。可以使用這個命令檢視查詢的開銷:
show status like 'Last_query_cost';
mysql官網關於這裡的,想了解更多的同學可以看看
4.2 優化器可以做什麼?
MySQL的優化器能處理哪些優化型別呢?舉兩個簡單的例子:
- 當我們對多張表進行關聯查詢的時候,以哪個表的資料作為基準表
- 有多個索引可以使用的時候,選擇哪個索引
實際上,對於每一種資料庫來說,優化器的模組都是必不可少的,他們通過複雜的演算法實現儘可能優化查詢效率的目標。如果對於優化器的細節感興趣,可以看看《資料庫查詢優化器的藝術-原理解析與SQL效能優化》。
但是優化器也不是萬能的,並不是再垃圾的SQL語句都能自動優化,也不是每次都能選擇到最優的執行計劃,大家在編寫SQL語句的時候還是要注意。
如果我們想知道優化器是怎麼工作的,它生成了幾種執行計劃,每種執行計劃的cost是多少,應該怎麼做?
4.3 優化器得到執行計劃的過程?
首先,我們要啟用優化器的追蹤(預設是關閉的):
SHOW VARIABLES LIKE 'optimizer_trace';
set optimizer_trace = 'enabled=on';
注意,開啟這開關是會消耗效能的,因為它要把優化分析的結果寫到表裡面,所以不要輕易開啟,或者檢視完之後關閉它(改成off)。接著我們執行一個SQL語句,優化器會生成執行計劃(下面是一個兩表聯查):
SELECT d.username,i.phone from user_info i,user_detail d WHERE i.id=d.user_id;
這個時候優化器分析的過程已經記錄到系統表裡面了,我們可以查詢
select * from information_schema.optimizer_trace\G;
如果是直接在Navicat中執行,那麼得到結果只是短短一行資料,並沒有完整的執行計劃
所以,此處應該是在命令列中執行:
得到的優化器分析的過程是一個JSON型別的資料,主要分成三部分,準備階段、優化階段和執行階段。
那具體的優化計劃在哪呢?在優化階段(“join_optimization”)中的 considered_execution_plans
最後,分析完記得關掉它:
set optimizer_trace="enabled=off";
SHOWVARIABLESLIKE'optimizer_trace';
優化完之後,得到一個什麼東西呢?
4.4 如何檢視最終執行計劃?
優化器最終會把解析樹變成一個查詢執行計劃,查詢執行計劃是一個數據結構。
當然,這個執行計劃是不是一定是最優的執行計劃呢?不一定,因為MySQL也有可能覆蓋不到所有的執行計劃。我們怎麼檢視MySQL的執行計劃呢?比如多張表關聯查詢,先查詢哪張表?在執行查詢的時候可能用到哪些索引,實際上用到了什麼索引?
MySQL提供了一個執行計劃的工具。我們在SQL語句前面加上EXPLAIN,就可以看到執行計劃的資訊。
5.儲存引擎,儲存資料
得到執行計劃以後,SQL語句是不是終於可以執行了?問題又來了:
- 從邏輯的角度來說,我們的資料是放在哪裡的,或者說放在一個什麼結構裡面?
- 執行計劃在哪裡執行?是誰去執行?
5.1 儲存引擎基本介紹
我們先回答第一個問題:在關係型資料庫裡面,資料是放在什麼結構裡面的?放在表Table裡面的,我們可以把這個表理解成Excel電子表格的形式。所以我們的表在儲存資料的同時,還要組織資料的儲存結構,這個儲存結構就是由我們的儲存引擎決定的,所以我們也可以把儲存引擎叫做表型別。
在MySQL裡面,支援多種儲存引擎,他們是可以替換的,所以叫做外掛式的儲存引擎。為什麼要搞這麼多儲存引擎呢?一種還不夠用嗎?這個問題先留著。
5.2 檢視儲存引擎
比如我們資料庫裡面已經存在的表,我們怎麼檢視它們的儲存引擎呢?
show table status from `forum`; --forum是指定資料庫名
另外,還通過DDL建表語句來檢視。
在MySQL裡面,我們建立的每一張表都可以指定它的儲存引擎,而不是一個數據庫只能使用一個儲存引擎。儲存引擎的使用是以表為單位的。而且,建立表之後還可以修改儲存引擎。
我們說一張表使用的儲存引擎決定我們儲存資料的結構,那在伺服器上它們是怎麼儲存的呢?我們先要找到資料庫存放資料的路徑:
show variables like 'datadir';
預設情況下,每個資料庫有一個自己資料夾,以forum資料庫為例。
任何一個儲存引擎都有一個frm檔案,這個是表結構定義檔案。不同的儲存引擎存放資料的方式不一樣,產生的檔案也不一樣,innodb 是 1 個,memory沒有,myisam是兩個。
這些儲存引擎的差別在哪呢?
5.3 儲存引擎比較
MyISAM 和InnoDB 是我們用得最多的兩個儲存引擎,在 MySQL 5.5 版本之前,預設的儲存引擎是MyISAM,它是MySQL自帶的。我們建立表的時候不指定儲存引擎,它就會使用MyISAM作為儲存引擎。MyISAM的前身是ISAM(IndexedSequentialAccessMethod:利用索引,順序存取資料的方法)。
5.5版本之後預設的儲存引擎改成了InnoDB,它是第三方公司為MySQL開發的。為什麼要改呢?最主要的原因還是InnoDB 支援事務,支援行級別的鎖,對於業務一致性要求高的場景來說更適合。
這個裡面又有Oracle和MySQL公司的一段恩怨情仇。
InnoDB本來是InnobaseOy公司開發的, 它和MySQLAB公司合作開源了InnoDB的程式碼。但是沒想到MySQL的競爭對手Oracle把InnobaseOy收購了。後來08年Sun公司(開發Java語言的Sun)收購了MySQLAB,09年Sun公司又被 Oracle 收購了,所以 MySQL,InnoDB 又是一家了。有人覺得 MySQL 越來越像Oracle,其實也是這個原因。
那麼除了這兩個我們最熟悉的儲存引擎,資料庫還支援其他哪些常用的儲存引擎呢?我們可以用這個命令檢視資料庫對儲存引擎的支援情況:
show engines;
其中有儲存引擎的描述和對事務、XA協議和Savepoints的支援。
- XA協議用來實現分散式事務(分為本地資源管理器,事務管理器)。
- Savepoints用來實現子事務(巢狀事務)。建立了一個Savepoints之後,事務就可以回滾到這個點,不會影響到建立Savepoints之前的操作。
這些資料庫支援的儲存引擎,分別有什麼特性呢?
-
MyISAM(3 個檔案)
應用範圍比較小。表級鎖定限制了讀/寫的效能,因此在Web 和資料倉庫配置中,它通常用於只讀或以讀為主的工作。
-
支援表級別的鎖(插入和更新會鎖表)。不支援事務
-
擁有較高的插入(insert)和查詢(select)速度
-
儲存了表的行數(count速度更快)。(怎麼快速向資料庫插入100萬條資料?我們有一種先用MyISAM插入資料,然後修改儲存引擎為InnoDB的操作)
-
適用:只讀之類的資料分析的專案
-
-
InnoDB(2 個檔案)
mysql5.7中的預設儲存引擎。 InnoDB是一個事務安全(與ACID相容)的MySQL儲存引擎,它具有提交、回滾和崩潰恢復功能來保護使用者資料。InnoDB行級鎖(不升級為更粗粒度的鎖)和Oracle風格的一致非鎖讀提高了多使用者併發性和效能。InnoDB將使用者資料儲存在聚集索引中,以減少基於主鍵的常見查詢的I/O。為了保持資料完整性,InnoDB還支援外來鍵引用完整性約束。
-
支援事務,支援外來鍵,因此資料的完整性、一致性更高
-
支援行級別的鎖和表級別的鎖
-
支援讀寫併發,寫不阻塞讀(MVCC)
-
特殊的索引存放方式,可以減少IO,提升查詢效率
-
適用:經常更新的表,存在併發讀寫或者有事務處理的業務系統
-
-
Memory(1 個檔案)
將所有資料儲存在RAM中,以便在需要快速查詢非關鍵資料的環境中快速訪問。這個引擎以前被稱為堆引擎。其使用案例正在減少; InnoDB及其緩衝池記憶體區域提供了一種通用、持久的方法來將大部分或所有資料儲存在記憶體中,而ndbcluster為大型分散式資料集提供了快速的鍵值查詢。
- 把資料放在記憶體裡面,讀寫的速度很快,但是資料庫重啟或者崩潰,資料會全部消失。只適合做臨時表。
- 將表中的資料儲存到記憶體中
-
CSV(3 個檔案)
它的表實際上是帶有逗號分隔值的文字檔案。 csv表允許以csv格式匯入或轉儲資料,以便與讀寫相同格式的指令碼和應用程式交換資料。因為csv 表沒有索引,所以通常在正常操作期間將資料儲存在innodb表中,並且只在匯入或匯出階段使用csv表。
不允許空行,不支援索引。格式通用,可以直接編輯,適合在不同資料庫之間匯入匯出。
-
Archive(2 個檔案)
這些緊湊的未索引的表用於儲存和檢索大量很少引用的歷史、存檔或安全審計資訊。
不支援索引,不支援update delete.
這是MySQL裡面常見的一些儲存引擎,我們看到了,不同的儲存引擎提供的特性都不一樣,它們有不同的儲存機制、索引方式、鎖定水平等功能。我們在不同的業務場景中對資料操作的要求不同,就可以選擇不同的儲存引擎來滿足我們的需求,這個就是MySQL支援這麼多儲存引擎的原因。
5.4 如何選擇儲存引擎?
- 如果對資料一致性要求比較高,需要事務支援,可以選擇InnoDB。
- 如果資料查詢多更新少,對查詢效能要求比較高,可以選擇MyISAM。
- 如果需要一個用於查詢的臨時表,可以選擇Memory。
- 如果所有的儲存引擎都不能滿足你的需求,並且技術能力足夠,可以根據官網內部用C語言開發一個儲存引擎
6.執行引擎,返回結果
OK,儲存引擎分析完了,它是我們儲存資料的形式,繼續第二個問題,是誰使用執行計劃去操作儲存引擎呢?這就是我們的執行引擎,它利用儲存引擎提供的相應的API來完成操作。為什麼我們修改了表的儲存引擎,操作方式不需要做任何改變?因為不同功能的儲存引擎實現的API是相同的。
最後把資料返回給客戶端,即使沒有結果也要返回。
==> MySQL架構
基於上面分析的流程,我們一起來梳理一下MySQL的內部模組。
1.模組詳解
- Connector:用來支援各種語言和SQL的互動,比如PHP,Python,Java的 JDBC
- Management Serveices & Utilities:系統管理和控制工具,包括備份恢復、MySQL複製、叢集等等
- Connection Pool:連線池,管理需要緩衝的資源,包括使用者密碼許可權執行緒等等
- SQL Interface:用來接收使用者的SQL命令,返回使用者需要的查詢結果
- Parser:用來解析SQL語句
- Optimizer:查詢優化器
- CacheandBuffer:查詢快取,除了行記錄的快取之外,還有表快取,Key快取,許可權快取等等
- Pluggable Storage Engines:外掛式儲存引擎,它提供API給服務層使用,跟具體的檔案打交道
2.架構分層
總體上,我們可以把MySQL分成三層,跟客戶端對接的連線層,真正執行操作的服務層,和跟硬體打交道的儲存引擎層(參考MyBatis:介面、核心、基礎)。
-
連線層
我們的客戶端要連線到MySQL伺服器3306埠,必須要跟服務端建立連線,那麼管理所有的連線,驗證客戶端的身份和許可權,這些功能就在連線層完成。
-
服務層
連線層會把SQL語句交給服務層,這裡面又包含一系列的流程:比如查詢快取的判斷、根據SQL呼叫相應的介面,對我們的SQL語句進行詞法和語法的解析(比如關鍵字怎麼識別,別名怎麼識別,語法有沒有錯誤等等)。
然後就是優化器,MySQL底層會根據一定的規則對我們的 SQL語句進行優化,最後再交給執行器去執行。
-
儲存引擎
儲存引擎就是我們的資料真正存放的地方,在MySQL裡面支援不同的儲存引擎。再往下就是記憶體或者磁碟。