暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

巧用json工具解析MySQL优化器追踪文件

原创 听见风的声音 2025-07-16
253

1 MySQL数据库的优化器追踪

学习和研究Oracle优化器时会遇到10053事件,这是Oracle数据库中的一个跟踪事件,用于捕获优化器(CBO)的决策过程信息,这个事件是理解Oracle优化器的工作原理,分析和诊断SQL执行计划问题必不可少的工具。
MySQL也提供了类似的技术手段,MySQL优化器包含了执行追踪的能力,使用优化器追踪的接口通过一系列optimizer_trace_xxx系统变量和INFORMATION_SCHEMA.OPTIMIZER_TRACE表提供。基本步骤如下
1)激活优化器追踪

SET OPTIMIZER_TRACE="enabled=on",END_MARKERS_IN_JSON=on;

2)执行要追踪的SQL语句
下面这条语句也是本文要解析的SQL语句,是一个in条件的子查询

SELECT class_num, class_name FROM class WHERE class_num IN (SELECT class_num FROM roster);

3)查询OPTIMIZER_TRACE表

SELECT * FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE;

反复执行步骤2和3,默认一条语句的追踪会覆盖前一条追踪结果,可以通过optimizer_trace_offset和optimizer_trace_limit系统变量调整,保存多条语句的追踪结果,举例如下

--显示刚才执行的5条语句的追踪结果。 SET optimizer_trace_offset=-5, optimizer_trace_limit=5

4)关闭优化器追踪

SET OPTIMIZER_TRACE="enabled=off"

2 json解析工具

追踪MySQL优化器产生的文件是json格式的,简单语句的追踪文件也有上百行,复杂的语句的的追踪计划几百行上千行也不罕见。如果不借助合适的工具,分析起来十分困难,比如对齐、操作之间的关系等很难理清。好在有不少json的解析工具可以使用,这些工具中有离线的,不如sublime text里的pretty json扩展,也有在线的,这篇文章介绍一个比较好用的在线工具,以及如何使用这个工具分析、解析,从而理解MySQL的执行计划。这个工具的地址如下
json在线解析格式化
网页如下
image.png
左边栏内有一个向上的箭头,点击可以上传json文件,也可以将文件内容直接复制粘贴到左边的内容框内。右边栏内点击图标树,以树形结构显示json文件,鼠标指向一行时,如显示向左的小箭头则可以点击展开此行,显示向下的小箭头则可以将此行的下级内容收起。

3 语句的执行计划

语句中查询的两个表的行数

mysql> select count(*), 'class' from class -> union -> select count(*), 'roster' from roster; +----------+--------+ | count(*) | class | +----------+--------+ | 10 | class | | 3000 | roster | +----------+--------+ 2 rows in set (0.00 sec)

查看这条语句是,关闭了MySQL的半连接转换,优化器执行的典型的关联子查询语句的执行计划

mysql> explain SELECT class_num, class_name FROM class WHERE class_num IN (SELECT class_num FROM roster); +----+--------------------+--------+------------+----------------+---------------+-----------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+--------------------+--------+------------+----------------+---------------+-----------+---------+------+------+----------+-------------+ | 1 | PRIMARY | class | NULL | ALL | NULL | NULL | NULL | NULL | 10 | 100.00 | Using where | | 2 | DEPENDENT SUBQUERY | roster | NULL | index_subquery | class_num | class_num | 4 | func | 600 | 100.00 | Using index | +----+--------------------+--------+------------+----------------+---------------+-----------+---------+------+------+----------+-------------+ 2 rows in set, 1 warning (0.00 sec)

这是一个典型的关联子查询,执行计划有两个查询,第一个查询是对class表的全表扫描,然后以扫描出的每一行为条件,执行第二个关联子查询,使用索引class_num对roster进行访问。

mysql> show warnings; +-------+------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Level | Code | Message | +-------+------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Note | 1003 | /* select#1 */ select `airportdb`.`class`.`class_num` AS `class_num`,`airportdb`.`class`.`class_name` AS `class_name` from `airportdb`.`class` where <in_optimizer>(`airportdb`.`class`.`class_num`,<exists>(<index_lookup>(<cache>(`airportdb`.`class`.`class_num`) in roster on class_num))) | +-------+------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ 1 row in set (0.00 sec)

上面显示的优化器对查询执行的转换,可以看出,优化器将in操作转换成了exists,还可以看到的一点是,优化器对子查询进行了物化(将查询结果存在内存中)。使用tree格式显示执行计划也可以看到这一点

mysql> explain analyze SELECT distinct class.class_num, class.class_name     FROM class     INNER JOIN roster     WHERE class.class_num = roster.class_num;
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| -> Table scan on <temporary>  (cost=7.47..9.84 rows=10) (actual time=0.989..0.991 rows=5 loops=1)
    -> Temporary table with deduplication  (cost=7.21..7.21 rows=10) (actual time=0.987..0.987 rows=5 loops=1)
        -> Nested loop inner join  (cost=6.21 rows=10) (actual time=0.273..0.895 rows=5 loops=1)
            -> Table scan on class  (cost=1.25 rows=10) (actual time=0.0393..0.0472 rows=10 loops=1)
            -> Limit: 1 row(s)  (cost=6.4 rows=1) (actual time=0.0761..0.0761 rows=0.5 loops=10)
                -> Covering index lookup on roster using class_num (class_num=class.class_num)  (cost=6.4 rows=600) (actual time=0.0758..0.0758 rows=0.5 loops=10)
 |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

从执行计划可以看出,外表执行的全表扫描,返回10行数据,子查询执行的索引扫描,执行了10次。

4 执行计划追踪文件解析

将语句的优化器追踪文件上传到上面的在线解析工具网站,可以看到以树形结构显示的执行计划,将二级一下的内容都收起,只显示到执行计划的第二级,如下图

4.1 整体分析

1
image.png
从上图看出这个执行计划共有四个步骤。join_optimization有两个,这个应为这条语句包含一个子查询的缘故。还可以看到每个步骤的操作对象,select#:n,这里的n是同执行计划里的id相对应的。另外还可以看到的是每个步骤下的有几个子步骤,如join_preparation是3步。

4.2 主要步骤分析
4.2.1 join_preparation

image.png
展开后,join_preparation操作的执行对象是select#:1,即对class表的查询,执行三个操作,第一个操作也是一个join_preparation操作,第二个是查询的转换操作,第三个是查询的扩展操作。先看第一个join_preparation操作
image.png
这个操作的执行的对象是select#:2,即子查询,MySQL为每个join(对应一个select)操作执行一次prepare操作,这个操作执行的是子查询对查询的扩展。
image.png
上图同时显示了第二个和第三操作的详细信息,第二个操作时转换操作,将子查询从in语法转换成exist语法,第三个操作是查询扩展,将语句扩展成优化器理解的形式。

4.2.2 join_optimization

MySQL为每个join(select)执行一次优化,首先执行的是对主查询的优化
image.png
优化的对象是select#:1 ,共执行9个步骤
image.png
第一个步骤是条件处理,执行三个转换,等式传播,常量传播,无效条件移除(如1=1)。

{ "rows_estimation": [ { "table": "`class`", "range_analysis": { "table_scan": { "rows": 10, "cost": 4.1 }, "potential_range_indexes": [ { "index": "PRIMARY", "usable": true, "key_parts": [ "class_num" ] } ], "setup_range_conditions": [], "group_index_skip_scan": { "chosen": false, "cause": "not_group_by_or_distinct" }, "skip_scan_range": { "chosen": false, "cause": "disjuntive_predicate_present" } } } ] }

第二个是行评估操作,评估操作返回的行数和成本,这里分别对全表扫描、索引范围扫描、组索引跳扫、索引范围跳扫进行了评估,给出了不采用后面两个操作的原因。

{ "considered_execution_plans": [ { "plan_prefix": [], "table": "`class`", "best_access_path": { "considered_access_paths": [ { "rows_to_scan": 10, "access_type": "scan", "resulting_rows": 10, "cost": 2, "chosen": true } ] }, "condition_filtering_pct": 100, "rows_for_plan": 10, "cost_for_plan": 2, "chosen": true } ] }

这一步是考虑的执行计划,显示的是执行计划的信息,包括最佳访问路径等,chosen的值为true,指示出优化器选择了这个执行计划。

{ "finalizing_table_conditions": [ { "table": "`class`", "original_table_condition": "<in_optimizer>(`class`.`class_num`,<exists>(/* select#2 */ select `roster`.`class_num` from `roster` where (<cache>(`class`.`class_num`) = `roster`.`class_num`)))", "final_table_condition ": "<in_optimizer>(`class`.`class_num`,<exists>(/* select#2 */ select `roster`.`class_num` from `roster` where (<cache>(`class`.`class_num`) = `roster`.`class_num`)))" } ] }

这个部分显示表class最终的过滤条件。下面看对第二个join及子查询的优化
image.png
这个操作的优化步骤较多,有11步,和前面不同的多了第7步,潜在物化的执行计划以及对转换操作,这里只对和前一个操作有明显不同的进行分析

{ "ref_optimizer_key_uses": [ { "table": "`roster`", "field": "class_num", "equals": "<cache>(`class`.`class_num`)", "null_rejecting": true } ] }

这一步是优化器考虑的索引ref操作,即通过索引来访问表。

{ "considered_execution_plans": [ { "plan_prefix": [], "table": "`roster`", "best_access_path": { "considered_access_paths": [ { "access_type": "ref", "index": "class_num", "rows": 600, "cost": 61.3467, "chosen": true }, { "access_type": "scan", "cost": 307.909, "rows": 3000, "chosen": false, "cause": "cost" } ] }, "condition_filtering_pct": 100, "rows_for_plan": 600, "cost_for_plan": 61.3467, "chosen": true } ] }

优化器考虑的执行计划的信息,最佳访问路径选择了使用索引class_num的ref操作,它访问的行数和成本明显更低。

{ "transformation": { "select#": 2, "from": "IN (SELECT)", "to": "materialization", "has_nullable_expressions": false, "treat_UNKNOWN_as_FALSE": true, "possible": true } }

这是对子查询的转换,评估物化的可能性。

image.png

子查询物化的执行计划,物化的成本和exist的成本的比较,exist执行10次,每次61.3457,总成本是613.457,物化的成本是609.908,最终选择了物化。

```json
{
  "select#": 2,
  "from": "IN (SELECT)",
  "to": "materialization",
  "chosen": true,
  "unknown_key_1": {
    "creating_tmp_table": {
      "tmp_table_info": {
        "table": "<materialized_subquery>",
        "columns": 1,
        "row_length": 5,
        "key_length": 4,
        "unique_constraint": false,
        "makes_grouped_rows": false,
        "cannot_insert_duplicates": true,
        "location": "TempTable"
      }
    }
  }
}

创建子查询临时表的信息。

4.2.3 join_execution

image.png
join_execution的操作对象是select#:1,这个操作有10个子操作,即执行了10子查询操作(subselect_execution),主查寻返回的行数也是10行,这可以从语句的实际执行里Covering index lookup on roster using class_num这个操作的loops的值是10得到验证。
image.png
子查询的首次执行,这个操作有两步,第一步是子查询的物化,这一步执行一个操作,使用索引去重;第二步是子查询第一个循环的执行。
image.png
这个是子查询的第二次执行,这一次执行可以利用前面物化的结果,只有一步,即执行子查询。

4.3 执行计划生成过程一览

放一个执行计划的整体图,看一下效果
image.png
image.png

5小结

通过分析过程可以看出,使用json工具,更容易从总体上把握优化器执行计划的生成过程,在分析的过程中,可以对树的每一级结构进行展开和收缩,也支持对每一级结构的单独复制,使对每一部分进行深度的分析也很容易。

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论