MySQL 優化器原來是這樣工做的

你們好,我是隻談技術不剪髮的 Tony 老師。咱們在 MySQL 體系結構中介紹了 MySQL 的服務器邏輯結構,其中查詢優化器(optimizer)負責生成 SQL 語句的執行計劃,是決定查詢性能的一個關鍵組件。本文將會深刻分析 MySQL 優化器工做的原理以及如何控制優化器來實現 SQL 語句的優化。html

優化器概述

MySQL 優化器使用基於成本的優化方式(Cost-based Optimization),以 SQL 語句做爲輸入,利用內置的成本模型和數據字典信息以及存儲引擎的統計信息決定使用哪些步驟實現查詢語句,也就是查詢計劃。mysql

optimizer
查詢優化和地圖導航的概念很是類似,咱們一般只須要輸入想要的結果(目的地),優化器負責找到最有效的實現方式(最佳路線)。須要注意的是,導航並不必定老是返回最快的路線,由於系統得到的交通數據並不多是絕對準確的;與此相似,優化器也是基於特定模型、各類配置和統計信息進行選擇,所以也不可能老是得到最佳執行方式。
git

從高層次來講,MySQL Server 能夠分爲兩部分:服務器層以及存儲引擎層。其中,優化器工做在服務器層,位於存儲引擎 API 之上。優化器的工做過程從語義上能夠分爲四個階段:github

  1. 邏輯轉換,包括否認消除、等值傳遞和常量傳遞、常量表達式求值、外鏈接轉換爲內鏈接、子查詢轉換、視圖合併等;
  2. 優化準備,例如索引 ref 和 range 訪問方法分析、查詢條件扇出值(fan out,過濾後的記錄數)分析、常量表檢測;
  3. 基於成本優化,包括訪問方法和鏈接順序的選擇等;
  4. 執行計劃改進,例如表條件下推、訪問方法調整、排序避免以及索引條件下推。

邏輯轉換

MySQL 優化器首先可能會以不影響結果的方式對查詢進行轉換,轉換的目標是嘗試消除某些操做從而更快地執行查詢。例如(數據來源):sql

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 語句的詳細介紹能夠參考這篇文章json

咱們也能夠經過優化器跟蹤進一步瞭解優化器的執行過程,例如:性能優化

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,執行階段。

優化器跟蹤還能夠顯示其餘基於成本優化的過程,後續咱們還會使用該功能。關閉優化器跟蹤功能的方式以下:oop

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`,‘liubei@shuguo.com’ 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 優化器採用基於成本的優化方式,簡化的步驟以下:

  1. 爲每一個操做指定一個成本;
  2. 計算每一個可能的執行計劃各個步驟的成本總和;
  3. 選擇總成本最小的執行計劃。

爲了找到最佳執行計劃,優化器須要比較不一樣的查詢方案。隨着查詢中表的數量增長,可能的執行計劃會呈現指數級增加;由於每一個表均可能使用全表掃描或者不一樣的索引訪問方法,鏈接查詢可能使用任意順序。對於少許表的鏈接查詢(一般少於 7 到 10 個)可能不會產生問題,可是更多的表可能會致使查詢優化的時間比執行時間還要長。

因此優化器不可能遍歷全部的執行方案,一種更靈活的優化方法是容許用戶控制優化器在查找最佳查詢計劃時的遍歷程度。通常來講,優化器評估的計劃越少,則編譯查詢所花費的時間就越少;但另外一方面,因爲優化器忽略了一些計劃,所以可能找到的不是最佳計劃。

控制優化程度

MySQL 提供了兩個系統變量,能夠用於控制優化器的優化程度:

  • optimizer_prune_level, 基於返回行數的評估忽略某些執行計劃,這種啓發式的方法能夠極大地減小優化時間並且不多丟失最佳計劃。所以,該參數的默認設置爲 1;若是確認優化器錯過了最佳計劃,能夠將該參數設置爲 0,不過這樣可能致使優化時間的增長。
  • optimizer_search_depth,優化器查找的深度。若是該參數大於查詢中表的數量,能夠獲得更好的執行計劃,可是優化時間更長;若是小於表的數量,能夠更快完成優化,但可能得到的不是最優計劃。例如,對於 十二、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性能優化》

相關文章
相關標籤/搜索