Photo by Merilin Kirsika Tedder from Pexels
MySQL作為一個流行的開源關係型資料庫管理系統,它可以執行在多種平臺上,支援多種儲存引擎,提供了靈活的資料操作和管理功能。MySQL的邏輯架構可以分為三層:連線層、服務層和引擎層,下方是網上流傳度很廣的一張架構圖。
需要注意的是, 上圖描述的是MySQL5.7及以前的邏輯架構,MySQL8.0中正式移除了查詢快取元件, 因為從收集的資料來看查詢快取的命中率很低,即使是在MySQL5.7中查詢快取這個選項也是預設關閉的,所以本篇文章就不對快取這款內容做解析了。具體可以檢視官方的一篇部落格:
事實上,如果不去關注其內部的細節,《高效能MySQL》一書中的這張簡圖也足夠讓我們對其邏輯架構有一個直觀的認知:
當用戶端傳送連線請求時,MySQL伺服器會在連線層接收請求,分配一個執行緒來處理該連線,隨後進行身份驗證。具體的功能如下:
使用者端連線的建立與處理:當用戶端發起連線請求時,MySQL會建立一個專用的執行緒(以作業系統級別的執行緒實現)來為該使用者端服務。這些服務執行緒使用執行緒池裡的長連線服務多個使用者請求,減少了執行緒切換的開銷。
安全認證:安全認證是連線層的另一項重要任務。當用戶端連線到MySQL伺服器時,伺服器首先需要驗證使用者端的身份。MySQL使用基於使用者名稱、主機和密碼的認證方式。在連線時,使用者端需要提供有效的使用者名稱、主機名和密碼,伺服器會根據在"mysql.user"表中的資料進行驗證,若通過,則建立連線。
連線資源管理:MySQL支援可設定的最大連線數。當到達最大連線數時,新的連線請求會被拒絕。符合條件的使用者端可以設定連線超時時間、使用者端閒置關閉時間等引數。同時,可以通過"mysql.user"表設定特定使用者對於資料庫的操作許可權。
執行緒管理:MySQL會自動建立和管理連線執行緒,其中包括以執行緒數作為上限的執行緒池。執行緒池的目的是複用連線執行緒,避免了執行緒切換和建立的開銷。此外,MySQL使用非同步I/O機制和協程,儘可能提高了並行和吞吐量。
服務層是MySQL中的核心元件,負責提供各種資料庫操作所需的基本功能,如SQL語法處理、事務管理、鎖管理等。
服務層負責從使用者端接收來自連線層的SQL查詢請求,並進行初始分析、解析和預處理。
MySQL的服務層負責事務管理,確保在執行一系列操作時,滿足原子性、一致性、隔離性和永續性這四個特性。事務管理涉及的主要功能包括:
MySQL優化器使用快取來提高查詢速度,包括:
引擎層負責儲存資料和執行SQL語句。MySQL支援多種儲存引擎,每種引擎各有特點,根據實際需求進行選用。當然,只要沒有非常明確的特殊需求就不需要更改儲存引擎,因為InnoDB在大部分場景下都比其他引擎更加適用。引擎層通過標準API與服務層互動,實現資料的儲存和查詢。
我們可以在SQL命令列中執行 show engines;
來檢視當前支援的儲存引擎:
mysql> show engines;
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| Engine | Support | Comment | Transactions | XA | Savepoints |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| FEDERATED | NO | Federated MySQL storage engine | NULL | NULL | NULL |
| MEMORY | YES | Hash based, stored in memory, useful for temporary tables | NO | NO | NO |
| InnoDB | DEFAULT | Supports transactions, row-level locking, and foreign keys | YES | YES | YES |
| PERFORMANCE_SCHEMA | YES | Performance Schema | NO | NO | NO |
| MyISAM | YES | MyISAM storage engine | NO | NO | NO |
| MRG_MYISAM | YES | Collection of identical MyISAM tables | NO | NO | NO |
| BLACKHOLE | YES | /dev/null storage engine (anything you write to it disappears) | NO | NO | NO |
| CSV | YES | CSV storage engine | NO | NO | NO |
| ARCHIVE | YES | Archive storage engine | NO | NO | NO |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
9 rows in set (0.00 sec)
SQL語句的執行流程可以簡單分為以下幾個步驟:
另外請注意,本篇文章依舊是在為後續寫MySQL優化流程做知識上的鋪墊,所以一些細節會簡單介紹,但實際的SQL優化思想會等到後面的文章再詳細介紹。
下面我們來詳細解釋一下SQL語句的執行流程和細節。
語法分析是MySQL執行SQL語句的第一步。語法分析器會對SQL語句進行分析,檢查其是否符合語法規則。如果SQL語句不符合語法規則,MySQL將會返回一個錯誤訊息。詳細的來說又可分為以下幾步:
MySQL使用的語法分析器是Bison。它是一種自動生成解析器的工具,可以根據語法規則自動生成語法分析器。下面是一個範例SQL語句:
SELECT name, age FROM student WHERE id = 1;
在語法分析階段,MySQL會進行以下操作:
token | type | value |
---|---|---|
SELECT | keyword | select |
name | identifier | name |
, | symbol | , |
age | identifier | age |
FROM | keyword | from |
student | identifier | student |
WHERE | keyword | where |
id | identifier | id |
= | operator | = |
1 | number | 1 |
select_statement: SELECT select_expression_list FROM table_reference_list [WHERE where_condition]
如果以上步驟都沒有出現錯誤,那麼MySQL就會認為這條SQL語句在語法分析階段是正確的,並繼續進行後續的處理。否則,MySQL就會報錯,並停止執行這條SQL語句。
查詢優化是MySQL執行SQL語句的第三步。SQL語句在查詢優化階段會經歷以下步驟:
舉例說明,下面是一個範例SQL語句:
SELECT name, age FROM student WHERE id IN (SELECT student_id FROM score WHERE score > 80);
則首先在查詢重寫時,MySQL會將這條SQL語句重寫為:
SELECT name, age FROM student s JOIN score c ON s.id = c.student_id WHERE c.score > 80;
這樣做的好處是:
接下來在查詢分解階段,MySQL會將這條SQL語句分解為兩個子查詢:
SELECT name, age, id FROM student;
SELECT student_id FROM score WHERE score > 80;
預處理時MySQL會對SQL語句進行一些基本的檢查和處理,例如檢查表名和欄位名是否存在,解析引數等。
最後優化器會根據統計資訊和成本模型,為SQL語句選擇一個最佳的執行計劃。
MySQL優化器是負責為SQL語句選擇一個最佳的執行計劃的模組。執行計劃包括了連線順序,存取方法,索引選擇,排序策略等。MySQL優化器是基於成本的優化器(cost-based optimizer),也就是說它會根據統計資訊和成本模型來估算不同執行計劃的代價,並選擇代價最小的那個。
MySQL優化器在選擇執行計劃時會考慮以下幾個方面:
例如,上面能夠重寫後的SQL語句應該是:
SELECT name, age FROM student s JOIN score c ON s.id = c.student_id WHERE c.score > 80;
MySQL優化器在選擇執行計劃時會進行以下操作:
這麼一通分析之後到底有點紙上談兵,接下來我們在MySQL 8.0 裡執行命令,畢竟實踐出真知。進入MySQL命令列,利用explain來檢視執行計劃:
mysql> SELECT name, age FROM student WHERE id IN (SELECT student_id FROM score WHERE score > 80);
+----------+------+
| name | age |
+----------+------+
| zhangsan | 18 |
| wangwu | 20 |
+----------+------+
2 rows in set (0.00 sec)
mysql> explain SELECT name, age FROM student WHERE id IN (SELECT student_id FROM score WHERE score > 80);
+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-----------------------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-----------------------------------------------------------------+
| 1 | SIMPLE | student | NULL | ALL | PRIMARY | NULL | NULL | NULL | 4 | 100.00 | NULL |
| 1 | SIMPLE | score | NULL | ALL | student_id | NULL | NULL | NULL | 8 | 12.50 | Using where; FirstMatch(student); Using join buffer (hash join) |
+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-----------------------------------------------------------------+
2 rows in set, 1 warning (0.01 sec)
我們可以在explain的結果中看到,兩行的ID值都為1,而我們自己寫的SQL語句裡有兩個Select,說明實際並沒有按照我們的原SQL來執行
再使用show warnings
來檢視實際執行的sql內容:
mysql> show warnings;
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message |
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Note | 1003 | /* select#1 */ select `datasets`.`student`.`name` AS `name`,`datasets`.`student`.`age` AS `age` from `datasets`.`student` semi join (`datasets`.`score`) where ((`datasets`.`score`.`student_id` = `datasets`.`student`.`id`) and (`datasets`.`score`.`score` > 80)) |
+-------+------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
可以發現,實際執行的時候用的是semi join,這是因為Semi Join返回的結果只包含主表(左表)中滿足連線條件的行,而不包含從表(右表)的任何資料。它的主要目的是通過減少要比較的資料量來提高查詢效能。通過使用Semi Join,可以避免將兩個表的所有資料進行連線,並僅僅關注滿足連線條件的部分資料。
但是對於開發人員來說,我們並不需要關注優化器內部的所有決策,因為涉及的因素太多了,所以我們從整體上來看知道大致的優化方向即可。
這裡也給出上面範例的建表語句,方便有心的讀者自行嘗試:
CREATE TABLE student
(
id INT PRIMARY KEY auto_increment,
name VARCHAR(20),
age INT
) charset = utf8mb4;
CREATE TABLE score
(
student_id INT,
course VARCHAR(20),
score INT,
FOREIGN KEY (student_id) REFERENCES student (id)
) charset = utf8mb4;
INSERT INTO student (id, name, age) VALUES (1, 'zhangsan', 18),(2, 'lisi', 19),(3, 'wangwu', 20),(4, 'zhaoliu', 21);
INSERT INTO score (student_id, course, score) VALUES (1, '數學', 85),(1, '語文', 90),(2, '數學', 75),(2, '語文', 80),(3, '數學', 95),(3, '語文', 100),(4, '數學', 65),(4, '語文', 70);
SELECT name, age FROM student WHERE id IN (SELECT student_id FROM score WHERE score > 80);
執行SQL語句是MySQL執行SQL語句的最後一步。簡單來說,執行器會按照執行計劃的步驟,逐步執行SQL語句。執行器會根據查詢語句,從磁碟讀取資料,並將其儲存在記憶體中。然後,執行器會對資料進行排序、分組、聚合等操作,最終生成查詢結果。
說的詳細一點,一些重要的步驟如下:
實際上,依舊可以通過MySQL命令列來了解其執行過程:
mysql> set profiling = 'ON';
Query OK, 0 rows affected, 1 warning (0.00 sec)
mysql> SELECT name, age FROM student WHERE id IN (SELECT student_id FROM score WHERE score > 80);
+----------+------+
| name | age |
+----------+------+
| zhangsan | 18 |
| wangwu | 20 |
+----------+------+
2 rows in set (0.00 sec)
mysql> show profile for query 1;
+--------------------------------+----------+
| Status | Duration |
+--------------------------------+----------+
| starting | 0.000094 |
| Executing hook on transaction | 0.000005 |
| starting | 0.000008 |
| checking permissions | 0.000005 |
| checking permissions | 0.000004 |
| Opening tables | 0.000088 |
| init | 0.000009 |
| System lock | 0.000009 |
| optimizing | 0.000012 |
| statistics | 0.000035 |
| preparing | 0.000076 |
| executing | 0.000065 |
| end | 0.000004 |
| query end | 0.000005 |
| waiting for handler commit | 0.000009 |
| closing tables | 0.000008 |
| freeing items | 0.000021 |
| cleaning up | 0.000010 |
+--------------------------------+----------+
18 rows in set, 1 warning (0.00 sec)
可以看到,命令執行結果非常詳細的列出了所有步驟,本文只是挑選了一部分來展開說。
具體結合到例子來說明,假設有一條SQL語句如下:
SELECT name, age FROM student s JOIN score c ON s.id = c.student_id WHERE c.score > 80 ORDER BY s.age LIMIT 10;
在執行階段,MySQL會進行以下操作:
InnoDB是MySQL的預設儲存引擎,它支援事務、行級鎖、外來鍵、MVCC等特性,提供了高效能和高可靠性的資料儲存方案。InnoDB的底層結構主要由兩部分組成:記憶體結構和磁碟結構。
圖片來源:https://dev.mysql.com/doc/refman/8.0/en/innodb-architecture.html
InnoDB的記憶體結構主要包括以下幾個部分:
如果對這部分內容感興趣可以看官方檔案,這裡只做一個簡單的介紹。
主要用於快取表資料和索引資料,加快存取速度。緩衝池是InnoDB記憶體結構中最重要的部分,通常佔用宿主機80%的記憶體。緩衝池被分成多個頁,每頁預設大小為16KB,每頁可以存放多條記錄。緩衝池中的頁按照LRU(最近最少使用)演演算法進行淘汰,同時也被分成兩個子連結串列:New Sublist和Old Sublist,分別存放存取頻繁和不頻繁的頁。
主要用於快取對非聚集索引的修改操作,減少磁碟I/O。寫緩衝是緩衝池的一部分,當對非聚集索引進行插入、刪除或更新時,不會立即修改磁碟上的索引頁,而是先記錄在寫緩衝中。當緩衝池中的資料頁被重新整理到磁碟時,會將寫緩衝中的修改操作合併到相應的索引頁中。
主要用於快取重做紀錄檔(Redo Log),保證事務的永續性。紀錄檔緩衝是一個迴圈使用的記憶體區域,預設大小為16MB,可以通過引數innodb_log_buffer_size來調整。當事務提交時,會將紀錄檔緩衝中的重做紀錄檔重新整理到磁碟上的重做紀錄檔檔案中。紀錄檔緩衝中的重做紀錄檔也會在以下情況下被重新整理:紀錄檔緩衝已滿、每秒鐘一次、每個事務檢查點一次。
主要用於加速等值查詢,提高查詢效率。自適應雜湊索引是InnoDB根據查詢頻率和模式自動建立的一種雜湊索引,可以將某些B+樹索引轉換為雜湊索引,從而減少樹的搜尋次數。自適應雜湊索引是可選的,可以通過引數innodb_adaptive來開啟或關閉。
表空間是InnoDB儲存引擎邏輯結構的最高層,所有的資料都存放在表空間中。表空間可以分為以下五種型別¹²³⁴⁵:
資料字典包含用於跟蹤物件,如表,索引,和列等後設資料的內部系統表²⁴。後設資料實際上位於InnoDB系統表空間中。InnoDB使用資料字典來管理和存取資料庫物件,並檢查使用者對物件的許可權。資料字典在資料庫啟動時載入到記憶體中,並在資料庫關閉時重新整理到磁碟上。
雙寫緩衝區位於系統表空間中的儲存區域,用於保證資料頁在寫入磁碟時不會損壞²⁴⁵。InnoDB在Buffer Pool中重新整理頁面時,會將資料頁寫入doublewrite緩衝區後才會寫入磁碟。如果在寫入OS Cache或者磁碟mysql程序奔潰後, InnoDB啟動崩潰恢復能從doublewrite找到完整的副本用來恢復。
重做紀錄檔是基於磁碟的資料結構,在崩潰恢復期間用於糾正不完整事務寫入的資料 。MySQL以迴圈方式寫入重做紀錄檔檔案,預設會產生ib_logfile0 和 ib_logfile1兩個檔案。InnoDB在提交事務之前重新整理事務的redo log,InnoDB使用組提交(group commit)技術來提高效能。重做紀錄檔記錄了資料頁的物理修改,而不是邏輯修改,這樣可以減少紀錄檔的大小和恢復的時間。重做紀錄檔可以通過innodb_log_file_size和innodb_log_files_in_group引數來調整大小和數量。
更改緩衝區是Buffer Pool中的一部分,用於快取對輔助索引頁的修改 。當InnoDB需要修改一個輔助索引頁時,如果該頁在Buffer Pool中,則直接修改;如果該頁不在Buffer Pool中,則將修改記錄在Change Buffer中,而不是從磁碟讀取該頁。這樣可以減少磁碟I/O操作,提高效能。Change Buffer中的修改會在後臺或者檢查點時合併到輔助索引頁中。Change Buffer的大小可以通過innodb_change_buffer_max_size引數來調整。
這部分簡單介紹即可,參考官方檔案:MySQL :: MySQL 8.0 參考手冊 :: 15.11.2 檔案空間管理
InnoDB的磁碟結構主要包括以下幾個部分:
參考資料: