MySQL 优化器原来是这样工作的

作者: 不剪发的Tony老师
毕业于北京航空航天大学,十多年数据库管理与开发经验,目前在一家全球性的金融公司从事数据库架构设计。CSDN学院签约讲师以及GitChat专栏作者。csdn上的博客收藏于以下地址:https://tonydong.blog.csdn.net

文章目录

            优化器概述
            逻辑转换
            基于成本的优化
                控制优化程度
                设置成本常量
                数据字典与统计信息
            控制优化行为
            优化器和索引提示
            总结

大家好,我是只谈技术不剪发的 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";



下表列出了一些逻辑转换的示例:

基于成本的优化

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