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性能优化》

相关文章
相关标签/搜索