Mysql如何选择最优的执行计划

Mysql的架构我们都知道它是分为服务层、引擎层和文件系统层,其架构图如下所示:

一条完成的sql执行流程图如下所示:

优化器组件根据sql的结构生成不同的执行计划,执行器最终选择最优或者执行效率最高的执行计划执行sql并返回数据。那么这个最优的执行计划是如何选择的呢?

1、初识Mysql的执行计划

创建表(order)的信息如下所示:

CREATE TABLE `order` ( 
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', 
  `user_id` bigint NOT NULL COMMENT '用户id', 
  `order_id` bigint NOT NULL  COMMENT '订单id',, 
  `num` int DEFAULT NULL  COMMENT '订单数量',, 
  PRIMARY KEY (`id`), 
  KEY `idx_userId` (`user_id`) COMMENT '用户id索引' 
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

添加测试数据:

(1)使用二级索引的执行计划

执行sql如下所示:

EXPLAIN format = tree  SELECT * from `order` where user_id = 1234;

sql的执行结果如下所示:

执行结果中的cost=0.35,Mysql认为我们当前的sql执行的成本是0.35。

(2)禁用二级索引的执行计划

执行忽略二级索引的sql如下所示:

EXPLAIN format = tree SELECT * from `order` IGNORE index(idx_userId) where user_id = 1234;

执行的结果如下所示:

(3)查询最终的sql执行计划

EXPLAIN SELECT * from `order` where user_id = 1234;

执行的结果如下所示:

通过上面的分析我们可以得知道sql:

SELECT * from `order` where user_id = 1234;

可以生成两条执行计划,一个是走索引(idx_userId)的执行计划,另一个是全表扫描的执行计划,并且通过sql分析我们发现两者的cost值不一样的,cost的值如下所示:

执行方式 cost
索引执行计划 0.35
全表扫面执行计划 0.45

然后Mysql就选择cost最小的执行计划来执行我们的sql。

2、Mysql选择执行计划的原理

Index lookup on order using idx_userId (user_id=1234)  (cost=0.35 rows=1)

cost = io_cost + engine_cost = 0.35

io_c ost

IO的传输成本,如 My ISAM、InnoDB存储引擎都是将数据和索引都存储到磁盘上的,当客户端查询表中的记录时需要先把数据或者索引加载到内存,从磁盘加载到内存的过程损耗的时间称之为IO成本。

engine _cost(存储引擎扫描的成本)

数据在查询出来之后,存储引擎需要进一步进行对比然后返回到服务层,如下图所示:

一个查询语句的执行要经过server层和存储引擎层,这两层在Mysql中对应了不同的常数表,以Mysql8.0.39为案例介绍,如下所示:

server_cost是server层成本参数表,表字段和数据如下所示:

disk_temptable_create_cost:默认值是20,表示创建基于磁盘的临时表的成本,如果增大这个值会让优化器尽量少的创建基于磁盘的临时表。

disk_temptable_row_cost:默认值0.5,向基于磁盘的临时表写入或读取一条记录的成本,如果增大这个值就会让优化器尽量少的创建基于磁盘的临时表。

key_compare_cost:默认值0.05,两条记录做比较操作的成本,多用在排序操作上,如果增大这个值的话会提升filesort的成本,让优化器可能更倾向于使用索引完成排序而不是filesort。

memory_temptable_create_cost:默认值 1.0,创建基于内存的临时表的成本,如果增大这个值的话会让优化器尽量少的创建基于内存的临时表。

memory_temptable_row_cost:默认值0.1,向基于内存的临时表写入或读取一条记录的成本,如果增大这个值的话会让优化器尽量少的创建基于内存的临时表。

row_evaluate_cost:检测一条记录是否符合搜索条件的成本,不管读取记录时需不需要检测是否满足搜索条件,即使是空数据,其成本都算是0.1。

engine_cost是存储引擎层的参数表,表字段和数据如下所示:

io_block_read_cost:默认值 1.0,从磁盘上读取一个块对应的成本。对于 InnoDB存储引擎来说,一个页就是一个块,不过对于MYISAM 存储引擎来说,默认是以 4096 字节作为一个块的。

memory_block_read_cost: 默认值0.25, 衡量的是从内存中读取一个块对应的成本。

无论是 engine _cost表还是server_cost表中的字段对应的值,都是可以根据实际需要进行重新设置的。

有了这些表的数据的记录之后,我们使用json方式查询信息更为全面的执行计划返回的情况,如下所示:

EXPLAIN format = json  SELECT * from `order` IGNORE index(idx_userId) where user_id = 1234;

执行的结果:

{ 
  "query_block": { 
    "select_id": 1, 
    "cost_info": { 
      "query_cost": "0.45" 
    }, 
    "table": { 
      "table_name": "order", 
      "access_type": "ALL", 
      "rows_examined_per_scan": 2, 
      "rows_produced_per_join": 2, 
      "filtered": "100.00", 
      "cost_info": { 
        "read_cost": "0.25", 
        "eval_cost": "0.20", 
        "prefix_cost": "0.45", 
        "data_read_per_join": "64" 
      }, 
      "used_columns": [ 
        "id", 
        "user_id", 
        "order_id", 
        "num" 
      ], 
      "attached_condition": "(`longxia`.`order`.`user_id` = 1234)" 
    } 
  } 
}

rows_examined_per_scan:查询一次全表扫面的行数

rows_produced_per_join:扇出数量

query_cost:查询总成本

cost_info:成本明细信息

prefix_cost:查询总成本 = read_cost(0.25) + eval _cost(0.20),也就是我们上面看到的cost=0.45的全表扫描成本的来源。

总结:

(1)Mysql的执行计划是Mysql根据cost来选择值最小的计划来执行的。

(2)Mysql中的表server_cost中记录server层执行计划相关的参数;engine_cost表记录了存储引擎层执行计划相关的参数。

(3)查询的sql前面添加format = json的方式可以获取到执行计划的详细信息,我们需要读懂执行计划中的参数含义。

(4)有时候明明添加了查询条件的索引但为什么没有走索引,此时我们不仅要考虑索引是否失效问题,也需要考虑是否为Mysql的执行计划中走索引的cost是否比全表扫面的cost大导致Mysql使用了全表扫描的执行计划。

4