EXPLAIN 使用

1,981 阅读6分钟

「这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战

分析 EXPLAIN 的结果

现在我们来分析一下 EXPLAIN 的结果。

explain select * from salaries where from_date ='1996-12-02';

image-20210828110401998

select_type 是 simple 表示这是一个简单查询;查询的表是 salaries;type 是 ALL 表示发生了全表扫描,全秒扫描的性能是最差的;possible_keys、key、key_len 都是空,说明没有使用索引;rows 执行这条 SQL 语句需要扫描两百多万行数据才能返回,filtered 的值是百分之十;最后 extra 的值是 Using where 表示使用了where 条件。

通过执行这条 SQL 语句,可以发现执行这条 SQL 语句需要花费 600 毫秒,说明这条 SQL 语句执行的性能比较差。

下面我们再来分析一条 SQL 语句。

image-20210828111739750

经过运行的结果来抗,从中可以发现 explain 展示了两行结果,当有多行结果的时候,这个 id 字段还是有用的,它可以描述 SQL 语句的执行过程。

如果 explain 的结果包含多个 id 值,比如 id 的值为 1 和 id 的值为 2,那么数字越大越先执行,也就是说 id 的值为 2 的先执行,id 的值为 1 的后执行。而对于相同 id 的行,那么会从上到下依次执行,这一点在前面也介绍过。

其中一条操作了 employees 表,另外一条操作了 salaries 表,由于我们起了别名,所以 table 这一列展示了别名。

从 type 来看,操作 employees 表的时候,是 const,这是一个非常好的级别,使用了主键索引,可能会使用组件,实际使用的也是组件,并且只扫描了一条数据,因为我们指定了条件去查询的。rows x filtered 为 1,也就是 MySQL 预估会使用 employees 表的一行数据和 salaries 的表去关联。

同理,操作 表的时候,type 的值是 ref,ref 的性能也是不错的。也使用了主键索引,最后经过估算,需要扫描 17 行数据。

可视化工具分析 SQL

这里我们介绍两款可视化工具,一款是 IntelliJ IDEA,只要这款工具怎么使用,相信很多小伙伴都知道,另一是 MySQL 官网提供一款 MySQL Workbench。下面我们一起来简单使用一下这两款工具。

使用 IntelliJ IDEA 分析 SQL。选中 SQL 语句,右键点击 Explain Plan。

image-20210829104308238

下图所展示的就是通过IntelliJ IDEA 分析 SQL 语句。

image-20210829104501776

此时,可以点击 Show Visualization,展示一个树状的结果。

image-20210829104724428

使用 MySQL 官方提供的 MySQL Workbench 可视化分析。MySQL Workbench 为数据库管理员、程序开发者和系统规划师提供可视化的Sql开发、数据库建模、以及数据库管理功能。

扩展的 EXPLAIN

EXPLAIN 可产生额外的更多的信息,可以通过在 EXPLAIN 语句后紧跟一条 SHOW WARNING 语句查看扩展信息,从而帮助我们了解 SQL 的更多详情。

扩展的 EXPLAIN 与 MySQL 的版本有很大的关系:

在MySQL 8.0.12及更高版本,扩展信息可用于 SELECT、DELETE、INSERT、REPLACE、UPDATE 语句;在 MySQL 8.0.12 之前,扩展信息仅适用于SELECT语句。

在 MySQL 5.6 及更低版本,需使用 EXPLAIN EXTENDED xxx 语句;而从 MySQL5.7 开始,无需添加EXTENDED 关键词。

使用实例:

 explain select * from employees e left join salaries s on e.emp_no = s.emp_no where e.emp_no=10001\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: e
   partitions: NULL
         type: const
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 4
          ref: const
         rows: 1
     filtered: 100.00
        Extra: NULL
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: s
   partitions: NULL
         type: ref
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 4
          ref: const
         rows: 17
     filtered: 100.00
        Extra: NULL
2 rows in set, 1 warning (0.00 sec)
​
mysql> show warnings\g
+-------+------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message                                                                                                                                                                                                                                                                                                                                                                                                                                               |
+-------+------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Note  | 1003 | /* select#1 */ select '10001' AS `emp_no`,'1953-09-02' AS `birth_date`,'Georgi' AS `first_name`,'Facello' AS `last_name`,'M' AS `gender`,'1986-06-26' AS `hire_date`,`employees`.`s`.`emp_no` AS `emp_no`,`employees`.`s`.`salary` AS `salary`,`employees`.`s`.`from_date` AS `from_date`,`employees`.`s`.`to_date` AS `to_date` from `employees`.`employees` `e` left join `employees`.`salaries` `s` on((`employees`.`s`.`emp_no` = 10001)) where 1 |
+-------+------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
​

注意:扩展的 EXPLAIN,使用 IntelliJ IDEA 是无法实现的,必须要在 MySQL 终端里面才可以实现。

通过上面的结果显示来看,MySQL 优化之后的 SQL 语句和原始的 SQL 语句差异还是挺大的。

由于 SHOW WARNING 的结果并不一定是一个有效的 SQL 语句,也不一定能够执行。

在上面实例中没出现特殊标记,在平时的项目中,扩展的 EXPLAIN 在输出中可能会出现以下特殊标记:

  • <auto_key>:自动生成的临时表 key。
  • (expr):表达式(如标量子查询)执行一次,并且将结果值保存在内存中供以后使用。对于由多个值组成的结果,可以创建一个临时表,将会会看到 的信息。
  • (query fragment):将子查询谓词转换为 EXISTS 谓词,然后将子查询转换为可以与 EXISTS 谓词一起使用 。
  • <in_optimizer>(query fragment):这是一个内部优化器对象,对用户没有任何意义。
  • <index_lookup>(query fragment):使用索引查找来处理查询片段,从而查找符合条件的行。
  • (condition, expr1, expr2):如果条件为 true,则取 expr1,否则取 expr2。
  • <is_not_null_test>(expr):验证表达式不为 NULL 的测试。
  • (query fragment):使用子查询实现。
  • materialized-subquery.col_name:在内部物化临时表中对 col_name 引用,以保存评估子查询的结果。
  • <primary_index_lookup>(query fragment):使用主键查找来处理查询片段以查找符合条件的行。
  • <ref_null_helper>(expr):这是一个内部优化器对象,对用户没有任何意义。
  • /* select#N */ select_stmt:将 SELECT 与在非扩展的 EXPLAIN 行相关联,其具有一输出 id 的值 N。
  • outer_tables semi join (inner_tables):半连接操作。 inner_tables 显示未拉出的表。
  • :表示为缓存中间结果而创建的内部临时表。

当某些表属于 const 或 system 类型时,这些表中的列所涉及的表达式将由提前评估涉,并不是所显示语句的一部分。但是,当使用 FORMAT=JSON,某些 const 表访问显示为 ref 使用常量值的访问。

估计查询性能

多数情况下,你可以通过计算磁盘的搜索次数来估算查询性能。对于比较小的表,通常可以在一次磁盘搜索中找到行(因为索引可能已经被缓存了),而对于更大的表,你可以使用 B-tree 索引进行估算︰你需要进行多少次查找才能找到行: log(row_count) / log(index_block_length / 3 * 2 / (index_length + data_pointer_length) ) + 1。

在MysQL中, index_block_length 通常是1024字节,数据指针一般是 4 字节。比方说,有一个 500,000 的表,key 是 3 字节,那么根据计算公式:log(500 ,000 ) /1g ( 1024/3*2/( 3+4) ) + 1 = 4 次搜索。

该索引将需要 500,000 x 7 X 3/2=5.2MB 的存储空间(假设典型的索引缓存的填充率是 2/3),因此你可以在内存中存放更多索引,可能只要一到两个调用就可以找到想要的行了。 但是,对于写操作,你需要四个搜索请求来查找在何处放置新的索引值,然后通常需要 2 次搜索来更新索引并写入行。

前面的讨论并不意味着你的应用性能会因为 logN 而缓慢下降。只要内容被 OS 或 MySQL 服务器缓存,随着表的变大,只会稍微变慢。在数据量变得太大而无法缓存后,将会变慢很多,直到你的应用程序受到磁盘搜索约束(按照 logN 增长)。为了避免这种情况,可以根据数据的增长而增加 key 的。对于MyISAM表,key 的缓存大小由名为 key_buffer_size 的