大家好,我是隻談技術不剪髮的 Tony 老師。我們在 MySQL 體系結構中介紹了 MySQL 的服務器邏輯結構,其中查詢優化器(optimizer)負責生成 SQL 語句的執行計劃,是決定查詢性能的一個關鍵組件。本文將會深入分析 MySQL 優化器工作的原理以及如何控制優化器來實現 SQL 語句的優化。
優化器概述
MySQL 優化器使用基於成本的優化方式(Cost-based Optimization),以 SQL 語句作爲輸入,利用內置的成本模型和數據字典信息以及存儲引擎的統計信息決定使用哪些步驟實現查詢語句,也就是查詢計劃。
查詢優化和地圖導航的概念非常相似,我們通常只需要輸入想要的結果(目的地),優化器負責找到最有效的實現方式(最佳路線)。需要注意的是,導航並不一定總是返回最快的路線,因爲系統獲得的交通數據並不可能是絕對準確的;與此類似,優化器也是基於特定模型、各種配置和統計信息進行選擇,因此也不可能總是獲得最佳執行方式。
從高層次來說,MySQL Server 可以分爲兩部分:服務器層以及存儲引擎層。其中,優化器工作在服務器層,位於存儲引擎 API 之上。優化器的工作過程從語義上可以分爲四個階段:
- 邏輯轉換,包括否定消除、等值傳遞和常量傳遞、常量表達式求值、外連接轉換爲內連接、子查詢轉換、視圖合併等;
- 優化準備,例如索引 ref 和 range 訪問方法分析、查詢條件扇出值(fan out,過濾後的記錄數)分析、常量表檢測;
- 基於成本優化,包括訪問方法和連接順序的選擇等;
- 執行計劃改進,例如表條件下推、訪問方法調整、排序避免以及索引條件下推。
邏輯轉換
MySQL 優化器首先可能會以不影響結果的方式對查詢進行轉換,轉換的目標是嘗試消除某些操作從而更快地執行查詢。例如(數據來源):
mysql> explain
-> select *
-> from employee
-> where salary > 10000 and 1=1;
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | employee | NULL | ALL | NULL | NULL | NULL | NULL | 25 | 33.33 | Using where |
+----+-------------+----------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
mysql> show warnings\G
*************************** 1. row ***************************
Level: Note
Code: 1003
Message: /* select#1 */ select `hrdb`.`employee`.`emp_id` AS `emp_id`,`hrdb`.`employee`.`emp_name` AS `emp_name`,`hrdb`.`employee`.`sex` AS `sex`,`hrdb`.`employee`.`dept_id` AS `dept_id`,`hrdb`.`employee`.`manager` AS `manager`,`hrdb`.`employee`.`hire_date` AS `hire_date`,`hrdb`.`employee`.`job_id` AS `job_id`,`hrdb`.`employee`.`salary` AS `salary`,`hrdb`.`employee`.`bonus` AS `bonus`,`hrdb`.`employee`.`email` AS `email` from `hrdb`.`employee` where (`hrdb`.`employee`.`salary` > 10000.00)
1 row in set (0.00 sec)
顯然,查詢條件中的 1=1 是完全多餘的。沒有必要爲每一行數據都執行一次計算;刪除這個條件也不會影響最終的結果。執行EXPLAIN
語句之後,通過SHOW WARNINGS
命令可以查看邏輯轉換之後的 SQL 語句,從上面的結果可以看出 1=1 已經不存在了。
📝關於 MySQL 執行計劃和 EXPLAIN 語句的詳細介紹可以參考這篇文章。
我們也可以通過優化器跟蹤進一步瞭解優化器的執行過程,例如:
mysql> SET optimizer_trace="enabled=on";
Query OK, 0 rows affected (0.03 sec)
mysql> select * from employee where emp_id = 1 and dept_id = emp_id;
+--------+----------+-----+---------+---------+------------+--------+----------+----------+-------------------+
| emp_id | emp_name | sex | dept_id | manager | hire_date | job_id | salary | bonus | email |
+--------+----------+-----+---------+---------+------------+--------+----------+----------+-------------------+
| 1 | 劉備 | 男 | 1 | NULL | 2000-01-01 | 1 | 30000.00 | 10000.00 | liubei@shuguo.com |
+--------+----------+-----+---------+---------+------------+--------+----------+----------+-------------------+
1 row in set (0.00 sec)
mysql> select * from information_schema.optimizer_trace\G
*************************** 1. row ***************************
QUERY: select * from employee where emp_id = 1 and dept_id = emp_id
TRACE: {
"steps": [
{
"join_preparation": {
"select#": 1,
"steps": [
{
"expanded_query": "/* select#1 */ select `employee`.`emp_id` AS `emp_id`,`employee`.`emp_name` AS `emp_name`,`employee`.`sex` AS `sex`,`employee`.`dept_id` AS `dept_id`,`employee`.`manager` AS `manager`,`employee`.`hire_date` AS `hire_date`,`employee`.`job_id` AS `job_id`,`employee`.`salary` AS `salary`,`employee`.`bonus` AS `bonus`,`employee`.`email` AS `email` from `employee` where ((`employee`.`emp_id` = 1) and (`employee`.`dept_id` = `employee`.`emp_id`))"
}
]
}
},
{
"join_optimization": {
"select#": 1,
"steps": [
{
"condition_processing": {
"condition": "WHERE",
"original_condition": "((`employee`.`emp_id` = 1) and (`employee`.`dept_id` = `employee`.`emp_id`))",
"steps": [
{
"transformation": "equality_propagation",
"resulting_condition": "(multiple equal(1, `employee`.`emp_id`, `employee`.`dept_id`))"
},
{
"transformation": "constant_propagation",
"resulting_condition": "(multiple equal(1, `employee`.`emp_id`, `employee`.`dept_id`))"
},
{
"transformation": "trivial_condition_removal",
"resulting_condition": "multiple equal(1, `employee`.`emp_id`, `employee`.`dept_id`)"
}
]
}
},
...
]
}
},
{
"join_execution": {
"select#": 1,
"steps": [
]
}
}
]
}
MISSING_BYTES_BEYOND_MAX_MEM_SIZE: 0
INSUFFICIENT_PRIVILEGES: 0
1 row in set (0.00 sec)
優化器跟蹤輸出主要包含了三個部分:
- join_preparation,準備階段,返回了字段名擴展之後的 SQL 語句。對於 1=1 這種多餘的條件,也會在這個步驟被刪除;
- join_optimization,優化階段。其中 condition_processing 中包含了各種邏輯轉換,經過等值傳遞(equality_propagation)之後將條件 dept_id = emp_id 轉換爲了 dept_id = 1。另外 constant_propagation 表示常量傳遞,trivial_condition_removal 表示無效條件移除
- join_execution,執行階段。
優化器跟蹤還可以顯示其他基於成本優化的過程,後續我們還會使用該功能。關閉優化器跟蹤功能的方式如下:
SET optimizer_trace="enabled=off";
下表列出了一些邏輯轉換的示例:
原始語句 | 重寫形式 | 備註 |
---|---|---|
select * from employee where emp_id = 1; |
/* select#1 */ select ‘1’ AS `emp_id`,‘劉備’ AS `emp_name`,‘男’ AS `sex`,‘1’ AS `dept_id`,NULL AS `manager`,‘2000-01-01’ AS `hire_date`,‘1’ AS `job_id`,‘30000.00’ AS `salary`,‘10000.00’ AS `bonus`,‘[email protected]’ AS `email` from `hrdb`.`employee` where true | 通過主鍵或唯一索引進行等值查找時,在選擇執行計劃之前就完成了轉換,重寫爲查詢常量。 |
select * from employee where emp_id = 0; |
/* select#1 */ select NULL AS `emp_id`,NULL AS `emp_name`,NULL AS `sex`,NULL AS `dept_id`,NULL AS `manager`,NULL AS `hire_date`,NULL AS `job_id`,NULL AS `salary`,NULL AS `bonus`,NULL AS `email` from `hrdb`.`employee` where multiple equal(0, NULL) | 通過主鍵或唯一索引查找不存在的值。 |
select emp_name from employee e, (select * from department where dept_name =‘研發部’) as d where d.dept_id = e.dept_id and e.salary > 10000; |
/* select#1 */ select `hrdb`.`e`.`emp_name` AS `emp_name` from `hrdb`.`employee` `e` join `hrdb`.`department` where ((`hrdb`.`e`.`dept_id` = `hrdb`.`department`.`dept_id`) and (`hrdb`.`e`.`salary` > 10000.00) and (`hrdb`.`department`.`dept_name` = ‘研發部’)) | 派生表子查詢轉換爲連接查詢 |
基於成本的優化
MySQL 優化器採用基於成本的優化方式,簡化的步驟如下:
- 爲每個操作指定一個成本;
- 計算每個可能的執行計劃各個步驟的成本總和;
- 選擇總成本最小的執行計劃。
爲了找到最佳執行計劃,優化器需要比較不同的查詢方案。隨着查詢中表的數量增加,可能的執行計劃會呈現指數級增長;因爲每個表都可能使用全表掃描或者不同的索引訪問方法,連接查詢可能使用任意順序。對於少量表的連接查詢(通常少於 7 到 10 個)可能不會產生問題,但是更多的表可能會導致查詢優化的時間比執行時間還要長。
所以優化器不可能遍歷所有的執行方案,一種更靈活的優化方法是允許用戶控制優化器在查找最佳查詢計劃時的遍歷程度。一般來說,優化器評估的計劃越少,則編譯查詢所花費的時間就越少;但另一方面,由於優化器忽略了一些計劃,因此可能找到的不是最佳計劃。
控制優化程度
MySQL 提供了兩個系統變量,可以用於控制優化器的優化程度:
- optimizer_prune_level, 基於返回行數的評估忽略某些執行計劃,這種啓發式的方法可以極大地減少優化時間而且很少丟失最佳計劃。因此,該參數的默認設置爲 1;如果確認優化器錯過了最佳計劃,可以將該參數設置爲 0,不過這樣可能導致優化時間的增加。
- optimizer_search_depth,優化器查找的深度。如果該參數大於查詢中表的數量,可以得到更好的執行計劃,但是優化時間更長;如果小於表的數量,可以更快完成優化,但可能獲得的不是最優計劃。例如,對於 12、13 個或者更多表的連接查詢,如果將該參數設置爲表的個數,可能需要幾小時或者幾天時間才能完成優化;如果將該參數修改爲 3 或者 4,優化時間可能少於 1 分鐘。該參數的默認值爲 62;如果不確定是否合適,可以將其設置爲 0,讓優化器自動決定搜索的深度。
設置成本常量
MySQL 優化器計算的成本主要包括 I/O 成本和 CPU 成本,每個步驟的成本由內置的“成本常量”進行估計。另外,這些成本常量可以通過 mysql 系統數據庫中的 server_cost 和 engine_cost 兩個表進行查詢和設置。
server_cost 中存儲的是常規服務器操作的成本估計值:
select * from mysql.server_cost;
cost_name |cost_value|last_update |comment|default_value|
----------------------------|----------|-------------------|-------|-------------|
disk_temptable_create_cost | |2018-05-17 10:12:12| | 20.0|
disk_temptable_row_cost | |2018-05-17 10:12:12| | 0.5|
key_compare_cost | |2018-05-17 10:12:12| | 0.05|
memory_temptable_create_cost| |2018-05-17 10:12:12| | 1.0|
memory_temptable_row_cost | |2018-05-17 10:12:12| | 0.1|
row_evaluate_cost | |2018-05-17 10:12:12| | 0.1|
cost_value 爲空表示使用 default_value。其中,
- disk_temptable_create_cost 和 disk_temptable_row_cost 代表了在基於磁盤的存儲引擎(InnoDB 或 MyISAM)中使用內部臨時表的評估成本。增加這些值會使得優化器傾向於較少使用內部臨時表的查詢計劃。
- key_compare_cost 代表了比較記錄鍵的評估成本。增加該值將導致需要比較多個鍵值的查詢計劃變得更加昂貴。例如,執行 filesort 排序的查詢計劃比通過索引避免排序的查詢計劃相對更加昂貴。
- memory_temptable_create_cost 和 memory_temptable_row_cost 代表了在 MEMORY 存儲引擎中使用內部臨時表的評估成本。增加這些值會使得優化器傾向於較少使用內部臨時表的查詢計劃。
- row_evaluate_cost 代表了計算記錄條件的評估成本。增加該值會導致檢查許多數據行的查詢計劃變得更加昂貴。例如,與讀取少量數據行的索引範圍掃描相比,全表掃描變得相對昂貴。
engine_cost 中存儲的是特定存儲引擎相關操作的成本估計值:
select * from mysql.engine_cost;
engine_name|device_type|cost_name |cost_value|last_update |comment|default_value|
-----------|-----------|----------------------|----------|-------------------|-------|-------------|
default | 0|io_block_read_cost | |2018-05-17 10:12:12| | 1.0|
default | 0|memory_block_read_cost| |2018-05-17 10:12:12| | 0.25|
engine_name 表示存儲引擎,“default”表示所有存儲引擎,也可以爲不同的存儲引擎插入特定的數據。cost_value 爲空表示使用 default_value。其中,
- io_block_read_cost 代表了從磁盤讀取索引或數據塊的成本。增加該值會使讀取許多磁盤塊的查詢計劃變得更加昂貴。例如,與讀取較少塊的索引範圍掃描相比,全表掃描變得相對昂貴。
- memory_block_read_cost 與 io_block_read_cost 類似,但它表示從數據庫緩衝區讀取索引或數據塊的成本。
我們來看一個例子,執行以下語句:
explain format=json
select *
from employee
where dept_id between 4 and 5;
{
"query_block": {
"select_id": 1,
"cost_info": {
"query_cost": "2.75"
},
"table": {
"table_name": "employee",
"access_type": "ALL",
"possible_keys": [
"idx_emp_dept"
],
"rows_examined_per_scan": 25,
"rows_produced_per_join": 17,
"filtered": "68.00",
"cost_info": {
"read_cost": "1.05",
"eval_cost": "1.70",
"prefix_cost": "2.75",
"data_read_per_join": "9K"
},
"used_columns": [
"emp_id",
"emp_name",
"sex",
"dept_id",
"manager",
"hire_date",
"job_id",
"salary",
"bonus",
"email"
],
"attached_condition": "(`hrdb`.`employee`.`dept_id` between 4 and 5)"
}
}
}
查詢計劃顯示使用了全表掃描(access_type = ALL),而沒有選擇 idx_emp_dept。通過優化器跟蹤可以看到具體原因:
"analyzing_range_alternatives": {
"range_scan_alternatives": [
{
"index": "idx_emp_dept",
"ranges": [
"4 <= dept_id <= 5"
],
"index_dives_for_eq_ranges": true,
"rowid_ordered": false,
"using_mrr": false,
"index_only": false,
"rows": 17,
"cost": 6.21,
"chosen": false,
"cause": "cost"
}
],
"analyzing_roworder_intersect": {
"usable": false,
"cause": "too_few_roworder_scans"
}
}
使用全表掃描的總成本爲 2.75,使用範圍掃描的總成本爲 6.21。這是因爲查詢返回了 employee 表中大部分的數據,通過索引範圍掃描,然後再回表反而會比直接掃描表更慢。
接下來我們將數據行比較的成本常量 row_evaluate_cost 從 0.1 改爲 1,並且刷新內存中的值:
update mysql.server_cost
set cost_value=1
where cost_name='row_evaluate_cost';
flush optimizer_costs;
然後重新連接數據庫,再次獲取執行計劃的結果如下:
{
"query_block": {
"select_id": 1,
"cost_info": {
"query_cost": "38.51"
},
"table": {
"table_name": "employee",
"access_type": "range",
"possible_keys": [
"idx_emp_dept"
],
"key": "idx_emp_dept",
"used_key_parts": [
"dept_id"
],
"key_length": "4",
"rows_examined_per_scan": 17,
"rows_produced_per_join": 17,
"filtered": "100.00",
"index_condition": "(`hrdb`.`employee`.`dept_id` between 4 and 5)",
"cost_info": {
"read_cost": "21.51",
"eval_cost": "17.00",
"prefix_cost": "38.51",
"data_read_per_join": "9K"
},
"used_columns": [
"emp_id",
"emp_name",
"sex",
"dept_id",
"manager",
"hire_date",
"job_id",
"salary",
"bonus",
"email"
]
}
}
}
此時,優化器選擇的範圍掃描(access_type = range)。雖然它的成本增加爲 38.51,但是使用全表掃描的代價更高。
最後,記得將 row_evaluate_cost 的還原成默認設置並重新連接數據庫:
update mysql.server_cost
set cost_value= null
where cost_name='row_evaluate_cost';
flush optimizer_costs;
⚠️不要輕易修改成本常量,因爲這樣可能導致許多查詢計劃變得更糟!在大多數生產情況下,推薦通過添加優化器提示(optimizer hint)控制查詢計劃的選擇。
數據字典與統計信息
除了成本常量之外,MySQL 優化器在優化的過程中還會使用數據字典和存儲引擎中的統計信息。例如表的數據量、索引、索引的唯一性以及字段是否可空都會影響到執行計劃的選擇,包括數據的訪問方法和表的連接順序等。
MySQL 會在日常操作過程中粗略統計表的大小和索引的基數(Cardinality),我們也可以使用 ANALYZE TABLE 語句手動更新表的統計信息和索引的數據分佈。
ANALYZE TABLE tbl_name [, tbl_name] ...;
這些統計信息默認會持久化到數據字典表 mysql.innodb_index_stats 和 mysql.innodb_table_stats 中,也可以通過 INFORMATION_SCHEMA 視圖 TABLES、STATISTICS 以及 INNODB_INDEXES 進行查看。
另外,從 MySQL 8.0 開始增加了直方圖統計(histogram statistics),也就是字段值的分佈情況。用戶同樣可以通過ANALYZE TABLE
語句生成或者刪除字段的直方圖:
ANALYZE TABLE tbl_name
UPDATE HISTOGRAM ON col_name [, col_name] ...
[WITH N BUCKETS];
ANALYZE TABLE tbl_name
DROP HISTOGRAM ON col_name [, col_name] ...;
其中,WITH N BUCKETS 用於指定直方圖統計時桶的個數,取值範圍從 1 到 1024,默認爲 100。
直方圖統計主要用於沒有創建索引的字段,當查詢使用這些字段與常量進行比較時,MySQL 優化器會使用直方圖統計評估過濾之後的行數。例如,以下語句顯示了沒有直方圖統計時的優化器評估:
explain analyze
select *
from employee
where salary = 10000;
-> Filter: (employee.salary = 10000.00) (cost=2.75 rows=3) (actual time=0.612..0.655 rows=1 loops=1)
-> Table scan on employee (cost=2.75 rows=25) (actual time=0.455..0.529 rows=25 loops=1)
由於 salary 字段上既沒有索引也沒有直方圖統計,因此優化器評估返回的行數爲 3,但實際返回的行數爲 1。
我們爲 salary 字段創建直方圖統計:
analyze table employee update histogram on salary;
Table |Op |Msg_type|Msg_text |
-------------|---------|--------|-------------------------------------------------|
hrdb.employee|histogram|status |Histogram statistics created for column 'salary'.|
然後再次查看執行計劃:
explain analyze
select *
from employee
where salary = 10000;
-> Filter: (employee.salary = 10000.00) (cost=2.75 rows=1) (actual time=0.265..0.291 rows=1 loops=1)
-> Table scan on employee (cost=2.75 rows=25) (actual time=0.206..0.258 rows=25 loops=1)
此時,優化器評估的行數和實際返回的行數一致,都是 1。
MySQL 使用數據字典表 column_statistics 存儲字段值分佈的直方圖統計,用戶可以通過查詢視圖 INFORMATION_SCHEMA.COLUMN_STATISTICS 獲得直方圖信息:
select * from information_schema.column_statistics;
SCHEMA_NAME|TABLE_NAME|COLUMN_NAME|HISTOGRAM |
-----------|----------|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
hrdb |employee |salary |{"buckets": [[4000.00, 0.08], [4100.00, 0.12], [4200.00, 0.16], [4300.00, 0.2], [4700.00, 0.24000000000000002], [4800.00, 0.28], [5800.00, 0.32], [6000.00, 0.4], [6500.00, 0.48000000000000004], [6600.00, 0.52], [6800.00, 0.56], [7000.00, 0.600000000000000|
刪除以上直方圖統計的命令如下:
analyze table employee drop histogram on salary;
索引和直方圖之間的區別在於:
- 索引需要隨着數據的修改而更新;
- 直方圖通過命令手動更新,不會影響數據更新的性能。但是,直方圖統計會隨着數據修改變得過時。
相對於直方圖統計,優化器會優先選擇索引範圍優化評估返回的數據行。因爲對於索引字段而言,範圍優化可以獲得更加準確的評估。
控制優化行爲
MySQL 提供了一個系統變量 optimizer_switch,用於控制優化器的優化行爲。
select @@optimizer_switch;
@@optimizer_switch |
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
index_merge=on,index_merge_union=on,index_merge_sort_union=on,index_merge_intersection=on,engine_condition_pushdown=on,
index_condition_pushdown=on,mrr=on,mrr_cost_based=on,block_nested_loop=on,batched_key_access=off,materialization=on,
semijoin=on,loosescan=on,firstmatch=on,duplicateweedout=on,subquery_materialization_cost_based=on,use_index_extensions=on,
condition_fanout_filter=on,derived_merge=on,use_invisible_indexes=off,skip_scan=on,hash_join=on|
它的值由一組標識組成,每個標識的值都可以爲 on 或 off,表示啓用或者禁用了相應的優化行爲。
該變量支持全局和會話級別的設置,可以在運行時進行更改。
SET [GLOBAL|SESSION] optimizer_switch='command[,command]...';
其中,command 可以是以下形式:
- default,將所有優化行爲設置爲默認值。
- opt_name=default,將指定優化行爲設置爲默認值。
- opt_name=off,禁用指定的優化行爲。
- opt_name=on,啓用指定的優化行爲。
我們以索引條件下推(index_condition_pushdown)優化爲例,演示修改 optimizer_switch 的效果。首先執行以下語句查看執行計劃:
explain
select *
from employee e
where e.email like 'zhang%';
id|select_type|table|partitions|type |possible_keys|key |key_len|ref|rows|filtered|Extra |
--|-----------|-----|----------|-----|-------------|------------|-------|---|----|--------|---------------------|
1|SIMPLE |e | |range|uk_emp_email |uk_emp_email|302 | | 2| 100.0|Using index condition|
其中,Extra 字段中的“Using index condition”表示使用了索引條件下推。
然後禁用索引條件下推優化:
set @@optimizer_switch='index_condition_pushdown=off';
然後再次查看執行計劃:
id|select_type|table|partitions|type |possible_keys|key |key_len|ref|rows|filtered|Extra |
--|-----------|-----|----------|-----|-------------|------------|-------|---|----|--------|-----------|
1|SIMPLE |e | |range|uk_emp_email |uk_emp_email|302 | | 2| 100.0|Using where|
Extra 字段變成了“Using where”,意味着需要訪問表中的數據然後再應用該條件過濾。如果使用優化器跟蹤,可以看到更詳細的差異。
優化器和索引提示
雖然通過系統變量 optimizer_switch 可以控制優化器的優化策略,但是一旦改變它的值,後續的查詢都會受到影響,除非再次進行設置。
另一種控制優化器策略的方法就是優化器提示(Optimizer Hint)和索引提示(Index Hint),它們只對單個語句有效,而且優先級比 optimizer_switch 更高。
優化器提示使用 /*+ … */ 註釋風格的語法,可以對連接順序、表訪問方式、索引使用方式、子查詢、語句執行時間限制、系統變量以及資源組等進行語句級別的設置。
例如,在沒有使用優化器提示的情況下:
explain
select *
from employee e
join department d on d.dept_id = e.dept_id
where e.salary = 10000;
id|select_type|table|partitions|type |possible_keys|key |key_len|ref |rows|filtered|Extra |
--|-----------|-----|----------|------|-------------|-------|-------|--------------|----|--------|-----------|
1|SIMPLE |e | |ALL |idx_emp_dept | | | | 25| 4.0|Using where|
1|SIMPLE |d | |eq_ref|PRIMARY |PRIMARY|4 |hrdb.e.dept_id| 1| 100.0| |
優化器選擇 employee 作爲驅動表,並且使用全表掃描返回 salary = 10000 的數據;然後通過主鍵查找 department 中的記錄。
然後我們通過優化器提示 join_order 修改兩個表的連接順序:
explain
select /*+ join_order(d, e) */ *
from employee e
join department d on d.dept_id = e.dept_id
where e.salary = 10000;
id|select_type|table|partitions|type|possible_keys|key|key_len|ref|rows|filtered|Extra |
--|-----------|-----|----------|----|-------------|---|-------|---|----|--------|------------------------------------------|
1|SIMPLE |d | |ALL |PRIMARY | | | | 6| 100.0| |
1|SIMPLE |e | |ALL |idx_emp_dept | | | | 25| 4.0|Using where; Using join buffer (hash join)|
此時,優化器選擇了 department 作爲驅動表;同時訪問 employee 時選擇了全表掃描。我們可以再增加一個索引相關的優化器提示 index:
explain
select /*+ join_order(d, e) index(e idx_emp_dept) */ *
from employee e
join department d on d.dept_id = e.dept_id
where e.salary = 10000;
id|select_type|table|partitions|type|possible_keys|key |key_len|ref |rows|filtered|Extra |
--|-----------|-----|----------|----|-------------|------------|-------|--------------|----|--------|-----------|
1|SIMPLE |d | |ALL |PRIMARY | | | | 6| 100.0| |
1|SIMPLE |e | |ref |idx_emp_dept |idx_emp_dept|4 |hrdb.d.dept_id| 5| 10.0|Using where|
最終,優化器選擇了通過索引 idx_emp_dept 查找 employee 中的數據。
需要注意的是,通過提示禁用某個優化行爲可以阻止優化器使用該優化;但是啓用某個優化行爲不代表優化器一定會使用該優化,它可以選擇使用或者不使用。
⚠️開發和測試過程可以使用優化器提示和索引提示,但是生產環境中需要小心使用。因爲實際數據和環境會隨着時間發生變化,而且 MySQL 優化器也會越來越智能,合理的參數配置定時的統計更新通常是更好地選擇。
索引提示爲優化器提供瞭如何選擇索引的信息,直接出現在表名之後:
tbl_name [[AS] alias]
USE {INDEX|KEY} [FOR {JOIN|ORDER BY|GROUP BY}] (index_name, ...)
| {IGNORE|FORCE} {INDEX|KEY} [FOR {JOIN|ORDER BY|GROUP BY}] (index_name, ...)
USE INDEX 提示優化器使用某個索引,IGNORE INDEX 提示優化器忽略某個索引,FORCE INDEX 強制使用某個索引。
例如,以下語句使用了 USE INDEX 索引提示:
explain
select *
from employee e use index (idx_emp_job)
join department d on d.dept_id = e.dept_id
where e.salary = 10000;
id|select_type|table|partitions|type |possible_keys|key |key_len|ref |rows|filtered|Extra |
--|-----------|-----|----------|------|-------------|-------|-------|--------------|----|--------|-----------|
1|SIMPLE |e | |ALL | | | | | 25| 10.0|Using where|
1|SIMPLE |d | |eq_ref|PRIMARY |PRIMARY|4 |hrdb.e.dept_id| 1| 100.0| |
雖然我們使用了索引提示,但是由於索引 idx_emp_job 和查詢完全無關,優化器最終還是沒有選擇使用該索引。
以下示例使用了 IGNORE INDEX 索引提示:
explain
select *
from employee e
join department d ignore index (PRIMARY)
on d.dept_id = e.dept_id
where e.salary = 10000;
id|select_type|table|partitions|type|possible_keys|key|key_len|ref|rows|filtered|Extra |
--|-----------|-----|----------|----|-------------|---|-------|---|----|--------|------------------------------------------|
1|SIMPLE |e | |ALL |idx_emp_dept | | | | 25| 10.0|Using where |
1|SIMPLE |d | |ALL | | | | | 6| 16.67|Using where; Using join buffer (hash join)|
IGNORE INDEX 使得優化器放棄了 department 的主鍵查找,最終選擇了 hash join 連接兩個表。該示例也可以通過優化器提示 no_index 實現:
explain
select /*+ no_index(d PRIMARY) */ *
from employee e
join department d
on d.dept_id = e.dept_id
where e.salary = 10000;
⚠️從 MySQL 8.0.20 開始,提供了等價形式的索引級別優化器提示,將來可能會廢棄傳統形式的索引提示。
總結
MySQL 優化器使用基於成本的優化方式,利用數據字典和統計信息選擇 SQL 語句的最佳執行方式。同時,MySQL 爲我們提供了控制優化器的各種選項,包括控制優化程度、設置成本常量、統計信息收集、啓用/禁用優化行爲以及使用優化器提示等。
如果覺得文章對你有用,歡迎訂閱我的專欄《MySQL性能優化》!