美文网首页MySQL
53-MySQL-性能分析-EXPLAIN-Extra

53-MySQL-性能分析-EXPLAIN-Extra

作者: 紫荆秋雪_文 | 来源:发表于2022-10-27 17:55 被阅读0次

    一、Extra

    顾名思义,Extra列是用来说明一些额外信息的,包含不适合在其他列中显示但十分重要的额外信息。可以通过这些额外信息来更准确的理解MySQL到底将如何执行给定的查询语句

    1.1、No tables used

    当查询语句的没有 FROM 子句时将会提示该额外信息

    • SQL
    EXPLAIN SELECT 1;
    

    1.2、Impossible WHERE

    查询语句的 WHERE 子句永远为 FALSE 时将会提示该额外信息

    • SQL
     EXPLAIN SELECT * FROM s1 WHERE 1 != 1;
    

    1.3、Using where

    不用读取表中所有信息,仅通过索引就可以获取所需数据,这发生在对表的全部的请求列都是同一个索引的部分的时候,表示MySQL服务器将在存储引擎检索行后再进行过滤。表明使用了 where 过滤。

    1.3.1、WHERE子句中有针对该表的搜索条件时

    当使用全表扫描执行对某个表的查询,并且该语句的WHERE子句中有针对该表的搜索条件时,在Extra列中会提示上述额外信息

    • SQL
    EXPLAIN SELECT * FROM s1 WHERE common_field = 'a';
    
    image.png

    1.3.2、WHERE子句中有针对该表的搜索条件,同时有索引条件

    当使用索引访问来执行对某个表的查询,并且该语句的WHERE子句中有除了改索引包含的列之外的其他搜索条件时,在Extra列中也会提示上述额外信息。比如下边这个查询虽然使用idx_key1索引执行查询,但是搜索条件中除了包含key1的搜索条件key1='a',还包含common_field的搜索条件,所以Extra列会显示Using where

    • SQL
    EXPLAIN SELECT * FROM s1 WHERE key1 = 'a' AND common_field = 'a';
    
    image.png

    1.4、No matching min/max row

    当查询列表处有MIN或者MAX聚集函数,但是并没有符合WHERE子句中的搜索条件的记录时,将会提示该额外信息

    • SQL
    EXPLAIN SELECT MIN(key1) FROM s1 WHERE key1 = 'abcdefg';
    
    image.png

    1.5、Using index

    当我们的查询列表以及搜索条件中只包含属于某个索引的列,也就是在可以使用索引覆盖的情况下,在Extra列将会提示该额外信息

    • SQL
    EXPLAIN SELECT key1 FROM s1 WHERE key1 = 'a';
    
    image.png

    1.5.1、二级索引直接使用 索引覆盖

    • SQL
    EXPLAIN SELECT id, key1 FROM s1 WHERE key1 = 'a';
    
    image.png
    • 分析

    由于 key1 是索引idx_key1的列,是一个二级索引,所以当查询 id, key1 时,由于二级索引中已经包含有id,key1,索引不需要回表,这也就是索引覆盖

    1.5.2、二级索引无法使用 索引覆盖

    • SQL
    EXPLAIN SELECT id, key1, key3 FROM s1 WHERE key1 = 'a';
    
    image.png

    1.6、Using index condition

    有些搜索条件中虽然出现了索引列,但却不能使用到索引

    • SQL
    DESC
    SELECT *
    FROM s1
    WHERE key1 > 'z'
      AND key1 LIKE '%b';
    
    image.png
    • key1 LIKE '%b'无法使用索引
    DESC
    SELECT *
    FROM s1
    WHERE key1 LIKE '%b';
    
    image.png
    • key1 > 'z'使用索引
    DESC
    SELECT *
    FROM s1
    WHERE key1 > 'z';
    
    image.png

    分析

    • 1、其中的 key1 > 'z' 可以使用到索引,但是 key1 LIKE '%b' 却无法使用到索引,在以前版本的MySQL中是按照下面步骤来执行这个查询的

      • 1.1、先根据 key1 > 'z' 这个条件,从二级索引 idx_key1 中获取到对应的二级索引记录
      • 1.2、根据上一步骤得到的二级索引记录中的主键值进行回表,找到完整的用户记录再检测该记录是否符合 key1 LIKE '%b' 这个条件,将符合条件的记录加入到最后的结果集
    • 2、但是虽然 key1 LIKE '%b' 不能组成范围区间参与range访问方法的执行,但这个条件毕竟只涉及到了key1列,所以MySQL把上边的步骤改进了一下

      • 2.1、先根据 key1 > 'z' 这个条件,定位到二级索引idx_key1中对应的二级索引记录
      • 2.2、对于指定的二级索引记录,先不着急回表,而是先检测一下该记录是否满足 key1 LIKE '%b' 这个条件,如果这个条件不满足,则该二级索引记录压根儿就没必要回表
      • 2.3、对于满足 key1 LIKE '%b' 这个条件的二级索引记录执行回表操作
    • 3、回表操作其实是一个随机IO,比较耗时,所以上述修改虽然只改进了一点点,但是可以省去好多回表操作的成本。MySQL把他们的这个改进称之为索引条件下推。如果在查询语句的执行过程中将要使用索引条件下推这个特性,在Extra列中将会显示Using index condition

    1.7、Using join buffer (Block Nested Loop)

    在连接查询执行过程中,当被驱动表不能有效的利用索引加快访问速度,MySQL一般会为其分配一块名为join buffer的内存块来加速查询速度,也就是我们所讲的基于块的嵌套循环算法

    • SQL
    EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.common_field = s2.common_field;
    
    image.png
    • 可以在对被驱动表s1的执行计划的 Extra 列显示了两个提示
      • 1、Using join buffer (hash join):这是因为表s1的访问不能有效利用索引,只好退而求其次,使用join buffer来减少对 s1 表的访问次数,从而提高性能
      • 2、Using where:可以看到查询语句中有一个 s1.common_field = s2.common_field条件,因为 s2是驱动表,s1是被驱动表,所以在访问s1表时,s2.common_field的值已经确定下来了,所以实际上查询s1表的条件就是s1.common_field = 一个常数,所以提示了 Using where额外信息

    1.8、Not exists

    当使用作(外)连接时,如果 WHERE 子句中包含要求被驱动表的某个列等于NULL值的搜索条件,而且那个列又是不允许存储NULL值的,那么在该表的执行计划的Extra 列就会提示Not exists额外信息

    • SQL
    EXPLAIN SELECT * FROM s1 LEFT JOIN s2 ON s1.key1 = s2.key1 WHERE s2.id IS NULL;
    
    image.png
    • 分析

    上述查询中 s1 表时驱动表,s2表是被驱动表,s2.id列是不允许存储NULL值的,而WHERE子句中又包含s2.id IS NULL的搜索条件,这意味着必定是驱动表的记录在被驱动表中找不到匹配ON子句条件的记录才会把该驱动表的记录加入到最终的结果集,所以对于某条驱动表中的记录来说,如果能在被驱动表中找到1条符合ON子句条件的记录,那么该驱动表的记录就不会被加入到最终的结果集,也就是说我们没有必要到被驱动表中找到全部符合ON子句条件的记录,这样可以稍微节省一点性能

    1.9、Using intersect(...) 、 Using union(...) 和 Using sort_union(...)

    1、如果执行计划的 Extra 列出现了Using intersect(...)提示,说明准备使用Intersect索引合并的方式执行查询,括号中的. . .表示需要进行索引合并的索引名称;
    2、如果出现了Using union(...)提示,说明准备使用Union索引合并的方式执行查询
    3、出现了Using sort_union(...)提示,说明准备使用Sort-Union索引合并的方式执行查询

    • SQL
    EXPLAIN SELECT * FROM s1 WHERE key1 = 'a' OR key3 = 'a';
    
    image.png
    • 其中Extra列就显示了Using union(idx_key1,idx_key3),表明MySQL即将使用idx_key1idx_key3这两个索引进行Union索引合并的方式执行查询

    1.10、Zero limit

    LIMIT子句的参数为 0 时,表示压根不打算从表中读出任何记录,将会提示该额外信息

    • SQL
    EXPLAIN SELECT * FROM s1 LIMIT 0;
    
    image.png

    1.11、Using filesort

    有一些情况下对结果集中的记录进行排序时可以使用到索引的

    • SQL
    EXPLAIN SELECT * FROM s1 ORDER BY key1 LIMIT 10;
    
    image.png
    • 分析

    这个查询语句可以利用idx_key1索引直接取出key1列的 10 条记录,然后再进行回表操作就好了。但是很多情况下排序操作无法使用到索引,只能在内存中(记录较少的时候)或者磁盘中(记录较多的时候)进行排序,MySQL把这种在内存中或者磁盘上进行排序的方式统称为文件排序。如果某个查询需要使用文件排序的方式执行查询,就会在执行计划的Extra列中显示Using filesort提示

    • SQL
    EXPLAIN SELECT * FROM s1 ORDER BY common_field LIMIT 10;
    
    image.png
    • 如果查询中需要使用filesort的方式进行排序的记录非常多,那么这个过程是很耗费性能的,最好想办法将使用文件排序的执行方式改为使用索引进行排序

    1.12、Using temporary

    在许多查询的执行过程中,MySQL可能会借助临时表来完成一些功能,比如去重排序之类的,比如我们在执行许多包含DISTINCTGROUP BYUNION等子句的查询过程中,如果不能有效利用索引来完成查询,MySQL很有可能寻求通过建立内部的临时表来执行查询。如果查询中使用到了内部的临时表,在执行计划的Extra列将会显示Using temporary提示

    • SQL
    EXPLAIN SELECT DISTINCT common_field FROM s1;
    
    image.png
    • GROUP BY
    EXPLAIN SELECT common_field, count(*) AS amount FROM s1
    GROUP BY common_field;
    
    image.png
    • 使用索引来替代掉使用临时表

    执行计划中出现Using temporary并不是一个好的征兆,因为建立与维护临时表要付出很大成本的,所以最好能使用索引来替代掉使用临时表

    • SQL
    EXPLAIN
    SELECT key1, COUNT(*) AS amount
    FROM s1
    GROUP BY key1;
    
    image.png

    1.13、小结

    • EXPLAIN不考虑各种 Cache
    • EXPLAIN不能显示MySQL在执行查询时所作的优化工作
    • EXPLAIN不会告诉你关于触发器存储过程的信息或用户自定义函数对查询的影响情况
    • 部分统计信息是估算的,并非精确值

    二、EXPLAIN四种输出格式

    EXPLAIN可以输出四种格式: 传统格式JSON格式TREE格式 以及 可视化输出 。用户可以根据需要选择适用于自己的格式

    2.1、JSON格式

    JSON格式:在EXPLAIN单词和真正的查询语句中间加上 FORMAT=JSON 。

    • 语法
    EXPLAIN FORMAT=JSON SELECT ....
    
    • EXPLAIN 的 Column 与 JSON 的对应关系 Column 与 JSON 的对应关系.png
    • 举例

    EXPLAIN FORMAT = JSON
    SELECT *
    FROM s1
             INNER JOIN s2 s2 ON s1.key1 = s2.key2
    WHERE s1.common_field = 'a';
    
    • JSON结果
    {
        "query_block":{
            "select_id":1,
            "cost_info":{
                "query_cost":"2102.20"
            },
            "nested_loop":[
                {
                    "table":{
                        "table_name":"s1",
                        "access_type":"ALL",
                        "possible_keys":[
                            "idx_key1"
                        ],
                        "rows_examined_per_scan":9895,
                        "rows_produced_per_join":989,
                        "filtered":"10.00",
                        "cost_info":{
                            "read_cost":"914.80",
                            "eval_cost":"98.95",
                            "prefix_cost":"1013.75",
                            "data_read_per_join":"2M"
                        },
                        "used_columns":[
                            "id",
                            "key1",
                            "key2",
                            "key3",
                            "key_part1",
                            "key_part2",
                            "key_part3",
                            "common_field"
                        ],
                        "attached_condition":"((`testbigdata`.`s1`.`common_field` = 'a') and (`testbigdata`.`s1`.`key1` is not null))"
                    }
                },
                {
                    "table":{
                        "table_name":"s2",
                        "access_type":"eq_ref",
                        "possible_keys":[
                            "idx_key2"
                        ],
                        "key":"idx_key2",
                        "used_key_parts":[
                            "key2"
                        ],
                        "key_length":"5",
                        "ref":[
                            "testbigdata.s1.key1"
                        ],
                        "rows_examined_per_scan":1,
                        "rows_produced_per_join":989,
                        "filtered":"100.00",
                        "index_condition":"(cast(`testbigdata`.`s1`.`key1` as double) = cast(`testbigdata`.`s2`.`key2` as double))",
                        "cost_info":{
                            "read_cost":"989.50",
                            "eval_cost":"98.95",
                            "prefix_cost":"2102.20",
                            "data_read_per_join":"1M"
                        },
                        "used_columns":[
                            "id",
                            "key1",
                            "key2",
                            "key3",
                            "key_part1",
                            "key_part2",
                            "key_part3",
                            "common_field"
                        ]
                    }
                }
            ]
        }
    }
    
    • read_cost 是由下边这两部分组成的
      • IO成本
      • 检测rows x (1 - filter)条记录的CPU成本

    rows和filter都是我们前边介绍执行计划的输出列,在JSON格式的执行计划中,rows相当于rows_examined_per_scan,filtered名称不变。

    • eval_cost 是这样计算的

    检测 rows × filter 条记录的成本

    • prefix_cost 就是单独查询 s1 表的成本,也就是:

    read_cost + eval_cost

    • data_read_per_join 表示在此次查询中需要读取的数据量

    • 对于 s2 表的 "cost_info" 部分是这样的

     "cost_info":{
                            "read_cost":"989.50",
                            "eval_cost":"98.95",
                            "prefix_cost":"2102.20",
                            "data_read_per_join":"1M"
                        }
    

    由于 s2 表是被驱动表,所以可能被读取多次,这里的 read_costeval_cost 是访问多次 s2 表后累加起来的值,大家主要关注里边儿的 prefix_cost 的值代表的是整个连接查询预计的成本,也就是单次查询 s1 表和多次查询 s2 表后的成本的和

    2.2、TREE格式

    TREE格式是8.0.16版本之后引入的新格式,主要根据查询的 各个部分之间的关系各部分的执行顺序 来描述如何查询

    • SQL
    EXPLAIN FORMAT =tree SELECT * FROM s1 INNER JOIN s2 ON s1.key1 = s2.key2 WHERE s1.common_field = 'a';
    

    三、 SHOW WARNINGS的使用

    需要在终端中执行,客户端可能无法输出信息

    在我们使用 EXPLAIN 语句查看了某个查询的执行计划后,紧接着还可以使用 SHOW WARNINGS 语句查看与这个查询的执行计划有关的一些扩展信息

    • SQL
    EXPLAIN
    SELECT s1.key1, s2.key1
    FROM s1
             LEFT JOIN s2 ON s1.key1 = s2.key1
    WHERE s2.common_field IS NOT NULL;
    
    image.png
    mysql> SHOW WARNINGS\G;
    *************************** 1. row ***************************
      Level: Warning
       Code: 1739
    Message: Cannot use ref access on index 'idx_key1' due to type or collation conversion on field 'key1'
    *************************** 2. row ***************************
      Level: Note
       Code: 1003
    Message: /* select#1 */ select `testbigdata`.`s1`.`key1` AS `key1`,`testbigdata`.`s2`.`key1` AS `key1` from `testbigdata`.`s1` join `testbigdata`.`s2` where ((`testbigdata`.`s2`.`common_field` is not null) and (`testbigdata`.`s1`.`key1` = `testbigdata`.`s2`.`key1`))
    2 rows in set (0.00 sec)
    
    • 简化
    SELECT s1.`key1` AS `key1`, s2.`key1` AS `key1`
    FROM s1 s1
             JOIN s2 s2
    WHERE ((s2.`common_field` IS NOT NULL) AND (s1.`key1` = s2.`key1`));
    

    SHOW WARNINGS展示出来的信息有三个字段,分别是LevelCodeMessage。最常见的就是Code为1003的信息,当Code值为1003时,Message字段展示的信息类似于查询优化器将我们的查询语句重写后的语句。比如我们上边的查询本来是一个左(外)连接查询,但是有一个 s2.common_field IS NOT NULL 的条件,这就会导致查询优化器把左(外)连接查询优化为内连接查询,从SHOW WARNINGSMessage字段也可以看出来原本的LEFT JOIN已经变成了JOIN
    注意!!!Message字段展示的信息类似于查询优化器将我们的查询语句重写后的语句,并不是等价于标准的查询语句,在很多情况下并不能直接拿到Message中的SQL运行,它只能作为帮助我们理解MySQL将如何执行查询语句的一个参考依据而已

    三、分析优化器执行计划:trace

    OPTIMIZER_TRACE是MySQL5.6引入的一项跟踪功能,它可以跟踪优化器做出的各种决策(如访问表的方法、各种开销计算、各种转换等),并将跟踪结果记录到INFORMATION_SCHEMA.OPTIMIZER_TRACE表中。次功能默认是关闭。开启 trace,并设置格式为 JSON,同时设置 trace 最大能够使用的内存大小,避免解析过程中因为默认内存过小而不能够完整展示

    • 开启 trace
    SET OPTIMIZER_TRACE = 'enabled=on',END_MARKERS_IN_JSON = on;
    
    SET OPTIMIZER_TRACE_MAX_MEM_SIZE = 1000000;
    
    • 开启后,可分析如下语句SELECT、INSERT、REPLACE、UPDATE、DELETE、EXPLAIN、SET、DECLARE、CASE、IF、RETURN、CALL
    • 测试
    SELECT *
    FROM student
    WHERE id < 10;
    
    • 查询 information_schema.optimizer_trace 就可以知道MySQL是如何执行SQL的
    *************************** 1. row ***************************
    #  第一部分:查询语句
    QUERY: select * from student where id < 10
    #  第二部分:QUERY字段对应语句的跟踪信息
    TRACE: {
      "steps": [
        {
          "join_preparation": {  # 预备工作
            "select#": 1,
            "steps": [
              {
                "expanded_query": "/* select#1 */ select `student`.`id` AS `id`,`student`.`stuno` AS `stuno`,`student`.`name` AS `name`,`student`.`age` AS `age`,`student`.`classid` AS `classid` from `student` where (`student`.`id` < 10)"
              }
            ] /* steps */
          } /* join_preparation */
        },
        {
          "join_optimization": {  #  进行优化
            "select#": 1,
            "steps": [
              {
                "condition_processing": {  #  条件处理
                  "condition": "WHERE",
                  "original_condition": "(`student`.`id` < 10)",
                  "steps": [
                    {
                      "transformation": "equality_propagation",
                      "resulting_condition": "(`student`.`id` < 10)"
                    },
                    {
                      "transformation": "constant_propagation",
                      "resulting_condition": "(`student`.`id` < 10)"
                    },
                    {
                      "transformation": "trivial_condition_removal",
                      "resulting_condition": "(`student`.`id` < 10)"
                    }
                  ] /* steps */
                } /* condition_processing */
              },
              {
                "substitute_generated_columns": {  #  替换生成的列
                } /* substitute_generated_columns */
              },
              {
                "table_dependencies": [  #  表的依赖关系
                  {
                    "table": "`student`",
                    "row_may_be_null": false,
                    "map_bit": 0,
                    "depends_on_map_bits": [
                    ] /* depends_on_map_bits */
                  }
                ] /* table_dependencies */
              },
              {
                "ref_optimizer_key_uses": [  #  使用键
                ] /* ref_optimizer_key_uses */
              },
              {
                "rows_estimation": [  #  行判断
                  {
                    "table": "`student`",
                    "range_analysis": {
                      "table_scan": {
                        "rows": 7727979,
                        "cost": 794484
                      } /* table_scan */,  #  扫描表
                      "potential_range_indexes": [  #  潜在的范围索引
                        {
                          "index": "PRIMARY",
                          "usable": true,
                          "key_parts": [
                            "id"
                          ] /* key_parts */
                        }
                      ] /* potential_range_indexes */,
                      "setup_range_conditions": [  #  设置范围条件
                      ] /* setup_range_conditions */,
                      "group_index_range": {
                        "chosen": false,
                        "cause": "not_group_by_or_distinct"
                      } /* group_index_range */,
                      "skip_scan_range": {
                        "potential_skip_scan_indexes": [
                          {
                            "index": "PRIMARY",
                            "usable": false,
                            "cause": "query_references_nonkey_column"
                          }
                        ] /* potential_skip_scan_indexes */
                      } /* skip_scan_range */,
                      "analyzing_range_alternatives": {  #  分析范围选项
                        "range_scan_alternatives": [
                          {
                            "index": "PRIMARY",
                            "ranges": [
                              "id < 10"
                            ] /* ranges */,
                            "index_dives_for_eq_ranges": true,
                            "rowid_ordered": true,
                            "using_mrr": false,
                            "index_only": false,
                            "in_memory": 4.61915e-05,
                            "rows": 9,
                            "cost": 1.91987,
                            "chosen": true
                          }
                        ] /* range_scan_alternatives */,
                        "analyzing_roworder_intersect": {
                          "usable": false,
                          "cause": "too_few_roworder_scans"
                        } /* analyzing_roworder_intersect */
                      } /* analyzing_range_alternatives */,
                      "chosen_range_access_summary": {  #  选择范围访问摘要
                        "range_access_plan": {
                          "type": "range_scan",
                          "index": "PRIMARY",
                          "rows": 9,
                          "ranges": [
                            "id < 10"
                          ] /* ranges */
                        } /* range_access_plan */,
                        "rows_for_plan": 9,
                        "cost_for_plan": 1.91987,
                        "chosen": true
                      } /* chosen_range_access_summary */
                    } /* range_analysis */
                  }
                ] /* rows_estimation */
              },
              {
                "considered_execution_plans": [  #  考虑执行计划
                  {
                    "plan_prefix": [
                    ] /* plan_prefix */,
                    "table": "`student`",
                    "best_access_path": {  #  最佳访问路径
                      "considered_access_paths": [
                        {
                          "rows_to_scan": 9,
                          "access_type": "range",
                          "range_details": {
                            "used_index": "PRIMARY"
                          } /* range_details */,
                          "resulting_rows": 9,
                          "cost": 2.81987,
                          "chosen": true
                        }
                      ] /* considered_access_paths */
                    } /* best_access_path */,
                    "condition_filtering_pct": 100,  #  行过滤百分比
                    "rows_for_plan": 9,
                    "cost_for_plan": 2.81987,
                    "chosen": true
                  }
                ] /* considered_execution_plans */
              },
              {
                "attaching_conditions_to_tables": {  #  将条件附加到表上
                  "original_condition": "(`student`.`id` < 10)",
                  "attached_conditions_computation": [
                  ] /* attached_conditions_computation */,
                  "attached_conditions_summary": [  #  /附加条件概要
                    {
                      "table": "`student`",
                      "attached": "(`student`.`id` < 10)"
                    }
                  ] /* attached_conditions_summary */
                } /* attaching_conditions_to_tables */
              },
              {
                "finalizing_table_conditions": [
                  {
                    "table": "`student`",
                    "original_table_condition": "(`student`.`id` < 10)",
                    "final_table_condition   ": "(`student`.`id` < 10)"
                  }
                ] /* finalizing_table_conditions */
              },
              {
                "refine_plan": [  #  精简计划
                  {
                    "table": "`student`"
                  }
                ] /* refine_plan */
              }
            ] /* steps */
          } /* join_optimization */
        },
        {
          "join_execution": {  #  执行
            "select#": 1,
            "steps": [
            ] /* steps */
          } /* join_execution */
        }
      ] /* steps */
    }
    #第三部分:跟踪信息过长时,被截断的跟踪信息的字节数。
    MISSING_BYTES_BEYOND_MAX_MEM_SIZE: 0  #  丢失的超出最大容量的字节
    
    #  第四部分:执行跟踪语句的用户是否有查看对象的权限。
    #  当不具有权限时,该列信息为1且TRACE字段为空,
    #  一般在 调用带有SQL SECURITY DEFINER的视图或者是存储过程的情况下,会出现此问题
    INSUFFICIENT_PRIVILEGES: 0  #  缺失权限
    1 row in set (0.00 sec)
    

    相关文章

      网友评论

        本文标题:53-MySQL-性能分析-EXPLAIN-Extra

        本文链接:https://www.haomeiwen.com/subject/tmjttdtx.html