對於一個服務端開發來說 MYSQL 可能是他使用最熟悉的數據庫工具,然而熟練掌握 MYSQL 語句的拼寫和卓越的多條件查詢不代表出現性能問題的時候你知道該怎么解決。致力於不當 SQL boby,我們從頭開始入門 MYSQL,講一些你可能不知道的 MYSQL。
1. 一條 SQL 之旅
現在有一條查詢用戶信息表的 SQL :
select * from user where uid = 100001;
這條 SQL 是如何從你的應用程序到達 MYSQL 服務器並執行,然后查到結果再帶給你的呢?要回答這個問題我們的看一下 MYSQL 的整體架構:
-
建立連接
首先客戶端與 MYSQL 服務器建立連接這個就不用說,客戶端發起請求經過三次握手之后與服務器建立 TCP 連接;服務器收到請求之后對輸入的用戶名密碼做權限校驗, 校驗通過之后后續的通信就基於這個長連接進行傳輸。
一般來說一個 MYSQL 服務端是可以對應多個客戶端的,所以在服務端可能會有很多個客戶端連接同時保持,可以通過
show processlist;
命令來查看當前服務端有多少個連接:mysql> show processlist; +---------+------+---------------------+-------------------+---------+-------+-------+------------------+ | Id | User | Host | db | Command | Time | State | Info | +---------+------+---------------------+-------------------+---------+-------+-------+------------------+ | 1753984 | test | 10.31.0.64:65264 | xxx | Sleep | 92 | | NULL | | 6348496 | test | 10.26.134.61:43080 | xxx | Sleep | 2 | | NULL | | 7201973 | test | 10.26.8.104:61642 | xxx | Sleep | 927 | | NULL | | 7201976 | test | 10.26.8.104:61650 | xxx | Sleep | 927 | | NULL | | 7414866 | test | 10.26.134.238:52010 | xxx | Sleep | 32 | | NULL | ...... ...... ...... +---------+------+---------------------+-------------------+---------+-------+-------+------------------+ 42 rows in set (0.00 sec)
上面有個
Time
參數:表示該連接已經多久沒有發生過數據傳輸。默認如果超過 8 小時沒有發生過數據傳輸服務端就會自動關閉該連接。可以通過wait_timeout
參數來設置超時時間。 -
查詢緩存
MySQL 查詢緩存是 MySQL 中比較獨特的一個緩存區域,用來緩存特定 Query 的整個結果集信息,且共享給所有客戶端。為了提高完全相同的 Query 語句的響應速度,MySQL Server 會對查詢語句進行 Hash 計算后,把得到的 hash 值與 Query 查詢的結果集對應存放在Query Cache 中。當 MySQL Server 打開 Query Cache 之后,MySQL Server 會對接收到的每一個 SELECT 語句通過特定的 Hash 算法計算該 Query 的 Hash 值,然后通過該 hash 值到 Query Cache 中去匹配。
查詢緩存相關的配置參數有如下:
mysql> show variables like '%query_cache%'; +------------------------------+---------+ | Variable_name | Value | +------------------------------+---------+ | have_query_cache | YES | --查詢緩存是否可用 | query_cache_limit | 1048576 | --可緩存具體查詢結果的最大值 | query_cache_min_res_unit | 4096 | --查詢緩存分配的最小塊的大小(字節) | query_cache_size | 599040 | --查詢緩存的大小 | query_cache_type | ON | --是否支持查詢緩存 | query_cache_wlock_invalidate | OFF | --控制當有寫鎖加在表上的時候,是否先讓該表相關的 Query Cache失效 +------------------------------+---------+ 6 rows in set (0.02 sec)
開啟緩存
mysql> set global query_cache_size = 600000; --設置緩存內存大小 mysql> set global query_cache_type = ON; --開啟查詢緩存
關閉緩存
mysql> set global query_cache_size = 0; --設置緩存內存大小為0, 即初始化是不分配緩存內存 mysql> set global query_cache_type = OFF; --關閉查詢緩存
-
分析器
如果查詢緩存未開啟或者為命中的情況則會走正常的查詢流程,第一步就是 sql 解析器,作用是將整個查詢語句變為 MYSQL 服務器能理解的語言。
-
詞法分析:將整個查詢分解為多個元素;
-
語法分析:尋找 sql 語法規則產生一個序列並執行這些代碼;
-
已經前兩步之后會產生一個解析樹,提供給優化器使用。
-
-
查詢優化器
優化器工作主要包括兩個部分:
- 邏輯優化;
- 物理優化。
邏輯優化階段:大牛們為了我們這些渣渣程序員操碎了心,怕你寫的 sql 不好查詢慢然后上網發帖 “MYSQL 垃圾”,“再也不用 MYSQL,我准備自己寫個數據庫”,於是默默在在后台給你整了個 sql 語句優化。優化內容主要有:
一定能帶來優化效果的:
- 連接的消除(外連接,嵌套連接)
- 語義優化
- 冗余操作剪枝
可能會帶來性能的提升但是需要根據代價進行選擇
- 借用索引來優化分組、排序等操作
- 合並分組
- 連接查詢條件下推
- 公式條件的提取
- 謂詞上推
物理優化階段: 邏輯優化主要是針對語法規則和語義的規整、合並、裁剪,那么到了物理優化階段就需要拿着 MYSQL 認為是比較完美的 sql 去查詢底層存儲單元。查詢物理存儲需要解決的問題包括:
- 單表掃描中什么樣的方式掃描效率最優
- 兩個表連接的時候如何 join 才能最快的獲取數據
- 多表連接的時候如何進行排序,是否要對每種組合都進行探索
早期物理優化階段使用基於關系代數規則和啟發式規則對查詢進行優化后就認為生辰的執行計划是最優的。后面引入了最小代價查詢方式對每一個可能的可執行方式進行評估找到代價最小的作為最優執行計划,目前數據庫的查詢通常是將這兩種方式融合到一起。
關於優化器部分是可以講上幾天都講不完的,在此我們只是簡單介紹。
-
執行器
經過上面階段已經將如何查詢得到數據的最優解拿到,執行器需要做的是根據指令去獲取數據。
2. MYSQL 數據存儲
MYSQL 作為一個數據庫管理工具最底層肯定是將數據存入磁盤的,那么數據是如何進入磁盤的,進入磁盤之后的格式是什么,用什么方式來管理這些數據,讓我們帶着種種疑團走進 “今日說法”,一起來揭開這個秘密。
2.1 存儲引擎
根據對數據存儲方式、使用方式的要求, MYSQL 提供了各種私人訂制方案來讓客戶爸爸滿意,這些技術方案通過使用不同的存儲機制、索引方式、鎖技巧最終得到不同的組合效果同而適配不同的應用場景,我們將這種組合得到的產物定義為:存儲引擎。
存儲引擎主要做了哪些事情呢,包括不限於:
- 用合適的格式存儲數據
- 提供數據查詢,更新的接口
- 各種條件下數據一致性的支持
- 索引機制的建立
- 提供數據備份、故障恢復、故障轉移的能力
對於上面這些基本要求的實現,MYSQL 提供了哪些方案呢?
MyISAM
ISAM :Indexed Sequential Access Method(有索引的順序訪問方法)。MyISAM 底層基於這個引擎做了一些改良,這是 MYSQL 5.5 版本之前默認數據庫引擎,在當時那個年代提供了一些 當時沒有但是很必要 的特性:
- 索引管理
- 字段管理
- 表鎖
當時這些功能可是沒有的,早期的程序員確實很辛苦啊,人家造輪子是真輪子用來跑的,現在造輪子也是輪子不過是站在巨人的肩膀看得更遠了吧。
MyISAM 引擎對每張表的存貯會歸結為三個文件:
- .frm:以表的名字開始,存儲表定義;
- .MYD:存儲表數據;
- .MYI:存儲表索引。
因為 MyISAM 誕生的年代比較早,那時候也沒有現在互聯網這么龐大的需求,所以放在現在來看它其實是有很多的缺點,比如:
- 鎖粒度太大,MyISAM 表鎖有兩種模式:表共享讀鎖,表獨占寫鎖。讀的時候他不會阻塞其他用戶對同一表讀的需求,但是在讀期間不能寫;在寫的時候,會同時阻塞其他用戶對表的讀寫操作。這個放在現在高並發的場景下肯定是個災難。
- MyISAM 不支持事務。這種鎖機制也注定了它不能支持事務,它生來就是為了 select 操作更快而優化的並且也是滿足當時時代的場景。
- 在崩潰恢復方面,因為各種備份機制沒有那么多,帶來的直接后果就是它不支持崩潰后的安全恢復。
MEMORY
內存表,很顯然這種表是為了讀取速度而生。它既不支持事務也不支持外鍵等等,內存表的優勢是讀取快,那缺點就很多了:
- 對於 Varchar 類型使用固定大小的長度存儲,浪費空間;
- 占用內存資源,可能會造成內存崩潰;
- 服務器重啟,數據丟失。
InnoDB
從 MYSQL 5.5 開始,InnoDB成為表的默認引擎。它具有有史以來堪稱完美的特點:行鎖設計、支持多版本隔離控制、支持外鍵、支持一致性非鎖定讀、同時對內存和 CPU 的使用也隨着新硬件的發展做了一定的優化。
InnoDB 引擎本身是基於磁盤存儲數據的,但是因為 CPU速度和磁盤速度之間巨大的差距,所以在 InnoDB 引擎中大量使用緩沖池技術來提高讀寫速度。整體可以分為兩個部分:
-
緩沖區
緩沖區的類型也是各種各樣,主要包括:
- Buffer Pool:緩沖池,在主內存中開辟的一個區域,在 InnoDB 讀數據的時候會先訪問這里,以減少磁盤訪問頻次;
- Change Buffer:寫緩沖區,避免每次增刪改都進行IO操作;
- Adaptive Hash Index:自適應哈希索引,使用索引關鍵字的前綴構建哈希索引,提升查詢速度;
- Log Buffer:日志緩沖區,保存要寫入磁盤上的日志文件的數據,緩沖區的內容定期刷新到磁盤。
-
磁盤數據
磁盤中的數據結構可以分為兩大類:表空間和重做日志。
表空間又可分為:
- The System Tablespace(系統表空間):存儲更改緩沖區;
- File-Per-Table Tablespaces(獨立表空間):存儲單個Innodb表的數據和索引;
- General Tablespaces(通用表空間):使用
CREATE TABLESPACE
創建的共享表; - Undo Tablespaces(undo表空間):存儲
undo
日志; - Temporary Tablespaces(臨時表空間):臨時表包括會話臨時表和全局臨時表。
重做日志,redo log 保存的就是 buffer pool 刷到磁盤的數據。
InnoDB 在磁盤中對應的文件結構比較多,除去 redo log 和 bin log之外的主要文件有:
- .opt:數據庫配置文件,包含數據庫字符集屬性;
- .frm:數據表元數據文件,不管是獨立表空間還是系統表空間,每個表都對應一個;
- .ibd:數據庫獨立表空間文件,如果是獨立表空間則對應一個.ibd文件,否則保存在系統表空間。
后面我們專門找一節來講 InnoDB 引擎,這里簡單概述。
3. 常用命令
那么對於我們使用的數據庫如何查看當前使用的引擎呢:
查看當前 MYSQL 版本所支持的引擎:
mysql> show engines;
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| Engine | Support | Comment | Transactions | XA | Savepoints |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| MEMORY | YES | Hash based, stored in memory, useful for temporary tables | NO | NO | NO |
| CSV | YES | CSV storage engine | NO | NO | NO |
| MyISAM | YES | MyISAM storage engine | NO | NO | NO |
| BLACKHOLE | YES | /dev/null storage engine (anything you write to it disappears) | NO | NO | NO |
| MRG_MYISAM | YES | Collection of identical MyISAM tables | NO | NO | NO |
| PERFORMANCE_SCHEMA | YES | Performance Schema | NO | NO | NO |
| ARCHIVE | YES | Archive storage engine | NO | NO | NO |
| FEDERATED | NO | Federated MySQL storage engine | NULL | NULL | NULL |
| InnoDB | DEFAULT | Supports transactions, row-level locking, and foreign keys | YES | YES | YES |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
9 rows in set (0.00 sec)
查看 MYSQL 默認的存儲引擎:
mysql> show variables like '%storage_engine%';
+----------------------------+--------+
| Variable_name | Value |
+----------------------------+--------+
| default_storage_engine | InnoDB |
| default_tmp_storage_engine | InnoDB |
| storage_engine | InnoDB |
+----------------------------+--------+
3 rows in set (0.01 sec)
查看某個表用了什么引擎(在顯示結果里參數engine后面的就表示該表當前用的存儲引擎):
mysql> show create table 表名;
修改表的存儲引擎:
ALTER TABLE 表名 ENGINE = INNODB;
修改默認存儲引擎
如果修改本次會話的默認存儲引擎(重啟后失效),只對本會話有效,其他會話無效:
mysql> set default_storage_engine=innodb;
Query OK, 0 rows affected (0.00 sec)
修改全局會話默認存儲引擎(重啟后失效),對所有會話有效:
mysql> set global default_storage_engine=innodb;
Query OK, 0 rows affected (0.00 sec)
希望重啟后也有效,即編輯 /etc/my.cnf
,[mysqld] 下面任意位置添加配置(所有對配置文件的修改,重啟后生效)
default-storage-engine = InnoDB
關於 MYSQL 的結構部分限於篇幅就簡單介紹,后面我們一一來看細節。