数据库
首页 > 数据库> > 【MySQL 文档翻译】理解查询计划

【MySQL 文档翻译】理解查询计划

作者:互联网

原文地址: 【MySQL 文档翻译】理解查询计划

欢迎访问我的博客: http://blog.duhbb.com/

官方文档

MySQL 官方文档地址: 8.8 Understanding the Query Execution Plan

引言

MySQL 优化器会根据 SQL 语句中的表, 列, 索引和 WHERE 子句中的条件的详细信息, 使用许多技术来有效地执行 SQL 查询. 可以在不读取所有行的情况下对一个巨大的表执行查询; 可以在不比较每个行组合的情况下执行涉及多个表的连接. 优化器选择执行最有效查询的一组操作称为 查询执行计划 (query execution plan), 也称为 EXPLAIN plan. 你的目标是认识到 EXPLAIN 计划表明查询已优化好, 如果发现一些低效的操作, 可以通过学习 SQL 语法和索引技术来改进查询计划.

使用 EXPLAIN 优化查询

EXPLAIN 语句提供有关 MySQL 如何执行目标语句的信息:

在 EXPLAIN 的帮助下, 可以查看应该在哪里为表添加索引, 以便通过使用索引查找行来更快地执行语句. 您还可以使用 EXPLAIN 检查优化器是否以最佳顺序连接表. 要提示优化器使用与语句中表命名顺序相对应的连接顺序, 请以 SELECT STRAIGHT_JOIN 语句开头, 而不是 SELECT. (请参阅 第 13.2.10 节 SELECT 语句.) 但是, STRAIGHT_JOIN 可能会阻止使用索引, 因为它禁用了 半连接转换. 看第 8.2.2.1 节 使用半连接转换优化 IN 和 EXISTS 子查询谓词.

优化器跟踪 有时可能会提供比 EXPLAIN 更详细的信息. 但是, 优化器跟踪格式和内容可能会因版本而异. 有关详细信息, 请参阅 MySQL 内部: 跟踪优化器.

如果您认为应该用到的索引在查询计划中确没有被使用, 请运行 ANALYZE TABLE 以更新表统计信息, 例如键的基数, 这可能会影响优化器所做的选择. 请参阅第 13.7.3.1 节 ANALYZE TABLE 语句.

Explain 的输出格式

EXPLAIN 为 SELECT 语句中使用的每个表返回一行信息. 它按照 MySQL 在处理语句时读取它们的顺序排列的. 这意味着 MySQL 从第一个表中读取一行, 然后在第二个表中找到匹配的行, 然后在第三个表中, 以此类推. 处理完所有表后, MySQL 会输出选定的列并通过表列表回溯, 直到找到匹配行较多的表. 从此表中读取下一行, 并继续处理下一个表.

Explain 输出的列

本节介绍由 EXPLAIN 生成的输出列. 后面的部分提供了有关 type 和 Extra 列的附加信息.

EXPLAIN 的每个输出行提供有关一个表的信息. 每一行都包含 表 8.1 解释输出列 中总结的值, 并在表后进行了更详细的描述. 列名显示在表格的第一列; 第二列提供了 FORMAT=JSON 使用时输出中显示的等效属性名称.

JSON 名称 含义
id select_id SELECT 标识符
select_type None SELECT 类型
table table_name 输出行的表
partitions partitions 匹配的分区
type access_type 联接 (join) 类型
possible_keys possible_keys 可供选择的索引
key key 实际选择的索引
key_len key_length 所选 key 的长度
ref ref 与索引比较的列
rows rows 估计要检查的行数
filtered filtered 按表条件过滤的行百分比 (过滤得越多说明效果越好)
Extra None 附加信息

JSON 格式的输出中的 NULL 不会在 JSON 格式的 EXPLAIN 信息中显示.

select_type 价值 JSON 名称 意义
SIMPLE 没有任何 简单 SELECT(不使用 UNION 或子查询)
PRIMARY 没有任何 最外层 SELECT
UNION 没有任何 UNION 中的第二个或靠后的 SELECT 的语句
DEPENDENT UNION dependent(true) UNION 中的第二个或后面 SELECT 的语句, 取决于外部查询
UNION RESULT union_result UNION 后的结果.
SUBQUERY 没有任何 子查询中的第一个 SELECT
DEPENDENT SUBQUERY dependent(true) 子查询中第一个 SELECT, 依赖于外部查询
DERIVED 没有任何 派生表
DEPENDENT DERIVED dependent(true) 派生表依赖于另一个表
MATERIALIZED materialized_from_subquery 物化子查询
UNCACHEABLE SUBQUERY cacheable( false) 一个子查询, 其结果无法缓存, 必须为外部查询的每一行重新计算
UNCACHEABLE UNION cacheable( false) UNION 中第二个或靠后的 SELECT, 属于不可缓存子查询 (请参阅 参考资料 UNCACHEABLE SUBQUERY)

DEPENDENT 一般指关联子查询的使用. 详情见 Section 13.2.11.7 Correlated Subqueries

DEPENDENT SUBQUERY 和计算和 UNCACHEABLE SUBQUERY 的计算不太一样.DEPENDENT SUBQUERY 中的子查询对外部上下文中的不同集合的值只计算一遍. 而 UNCACHEABLE SUBQUERY, 会对外部上下文中的每一行都重新计算.

解释连接类型

EXPLAIN 输出中的 type 列描述了表是如何连接的. 在 JSON 格式的输出中, 则放到了 access_type 属性. 以下列表描述了连接类型, 按从最佳到最差的顺序排列:

请参阅第 8.2.1.15 节 IS NULL 优化.

unique_subquery 只是一个索引查找功能, 完全替代子查询以提高效率.

哇偶, 好厉害, 感觉这个得掌握一下哦

解释额外信息

输出列 Extra 包含 EXPLAIN 有关 MySQL 如何解析查询的附加信息. 下面的列表解释了可以出现在此列中的值. 每个项目还为 JSON 格式的输出指示哪个属性显示该 Extra 值. 对于其中一些, 有一个特定的属性. 其他显示为 message 属性的文本.

如果您想尽可能快地进行查询, 请注意查看 Extra 值是否包含 Using filesortUsing 还是 Using temporary.

Extra 提供了更多的信息, 比如是否用了临时表, 是否走了文件排序

假设 t2.id 定义为 NOT NULL. 在这种情况下, MySQL 扫描 t1 并查找行 t2 中与 t1.id 值相等的行. 如果 MySQL 在 t2 中找到匹配的行 , 它就知道 t2.id 永远不可能为 NULL, 并且不会扫描具有相同值的其余行. 换句话说, 对于 t1 中的每一行, MySQL 只需要在 t2 中进行一次查找, 而不管在 中实际匹配了多少行.
在 MySQL 8.0.17 及更高版本中, 这也可以指示 WHERE 中的 NOT IN (subquery)NOT EXIST (subquery) 已在内部转换为反连接 (antijoin). 这将移除子查询并将其表带入最顶层查询的计划中, 从而提供改进的成本计划. 通过合并半连接 (semijoins) 和反连接 (antijoins), 优化器可以更自由地重新排序执行计划中的表, 在某些情况下会产生更快的计划.(牛逼了)
您可以通过 EXPLAIN 后执行 SHOW WARNINGS 后的 Message 列或在 EXPLAIN FORMAT=TREE 的输出中查看对给定查询是否执行反连接转换.
> 笔记
反连接是半连接 table_a 和 table_b 在 condition 上的补充. 反连接返回 table_a 中所有没有在 condition 上匹配 table_b 的所有行.

每次 t 顶部查询处理新行时, 派生表的内容都会重新实现以使其保持最新状态.

假设 MIN(c1) 可以通过读取一个索引行 MIN(c2) 来检索它, 并且可以通过从不同的索引读取一行来检索它. 也就是说, 对于每一列 c1 和 c2, 都存在一个索引, 其中该列是索引的第一列. 在这种情况下, 通过读取两个确定性行来返回一行.
如果要读取的行不确定, 则 Extra 不会出现此值. 考虑这个查询:
sql SELECT MIN(c2) FROM t1 WHERE c1 < = 10;
假设这 (c1, c2) 是一个覆盖索引. 使用此索引, c1 <= 10 必须扫描所有行以找到最小值 c2. 相比之下, 考虑这个查询:
sql SELECT MIN(c2) FROM t1 WHERE c1 = 10;
在这种情况下, 第一个索引行 c1 = 10 包含最小值 c2 . 只需读取一行即可生成返回的行.
对于维护每个表的精确行数的存储引擎 (例如 MyISAM, 但不是 InnoDB), 对于缺少 WHERE 子句的 COUNT(*) 或始终为真且没有 GROUP BY 子句的查询, Extra 可能会出现此值 .(这是一个隐式分组查询的实例, 其中存储引擎影响是否可以读取确定的行数.)

EXPLAIN 输出解释

通过获取 EXPLAIN 输出 rows 列中值的乘积, 您可以很好地了解连接的好坏. 这应该大致告诉您 MySQL 必须检查多少行才能执行查询. 如果您使用 max_join_size 系统变量限制查询, 则此行积还用于确定 SELECT 要执行哪些多表语句以及要中止哪些语句. 请参见第 5.1.1 节 配置服务器.

以下示例显示了如何根据 EXPLAIN 提供的信息逐步优化多表连接.

假设您有如下的 SELECT 语句, 并且您计划使用 EXPLAIN 命令对其进行检查 :

EXPLAIN SELECT tt.TicketNumber, tt.TimeIn,
               tt.ProjectReference, tt.EstimatedShipDate,
               tt.ActualShipDate, tt.ClientID,
               tt.ServiceCodes, tt.RepetitiveID,
               tt.CurrentProcess, tt.CurrentDPPerson,
               tt.RecordVolume, tt.DPPrinted, et.COUNTRY,
               et_1.COUNTRY, do.CUSTNAME
        FROM tt, et, et AS et_1, do
        WHERE tt.SubmitTime IS NULL
          AND tt.ActualPC = et.EMPLOYID
          AND tt.AssignedPC = et_1.EMPLOYID
          AND tt.ClientID = do.CUSTNMBR;

对于此示例, 作出以下假设:

数据类型
tt ActualPC CHAR(10)
tt AssignedPC CHAR(10)
tt ClientID CHAR(10)
et EMPLOYID CHAR(15)
do CUSTNMBR CHAR(15)
索引
tt ActualPC
tt AssignedPC
tt ClientID
et EMPLOYID(主键)
do CUSTNMBR(主键)

最初, 在执行任何优化之前, 该 EXPLAIN 语句会生成以下信息:

table type possible_keys key  key_len ref  rows  Extra
et    ALL  PRIMARY       NULL NULL    NULL 74
do    ALL  PRIMARY       NULL NULL    NULL 2135
et_1  ALL  PRIMARY       NULL NULL    NULL 74
tt    ALL  AssignedPC,   NULL NULL    NULL 3872
           ClientID,
           ActualPC
      Range checked for each record (index map: 0x23)

因为对于每个表 type 都是 ALL, 这个输出表明 MySQL 正在生成所有表的笛卡尔积; 也就是说所有行的组合. 这需要相当长的时间, 因为必须检查每个表中行数的乘积. 对于当前的情况, 这个乘积是 $74 × 2135 × 74 × 3872 = 45,268,558,720$ 行. 如果表更大, 您只能想象需要多长时间.

这里的一个问题是, 如果将列声明为相同的类型和大小, MySQL 可以更有效地使用列上的索引. 在这种情况下, 如果 VARCHAR 和 CHAR 被声明为相同的大小, 则认为它们是相同的.tt.ActualPC 被声明为 CHAR(10) , 而 et.EMPLOYID 被声明为 CHAR(15), 因此存在长度不匹配.

要修复列长度之间的这种差异, 请使用 ALTER TABLE 将 ActualPC 从 10 个字符延长到 15 个字符:

mysql> ALTER TABLE tt MODIFY ActualPC VARCHAR(15);

现在 tt.ActualPCet.EMPLOYID 都是 VARCHAR(15). 再次执行该 EXPLAIN 语句会产生以下结果:

table type   possible_keys key     key_len ref         rows    Extra
tt    ALL    AssignedPC,   NULL    NULL    NULL        3872    Using
             ClientID,                                         where
             ActualPC
do    ALL    PRIMARY       NULL    NULL    NULL        2135
      Range checked for each record (index map: 0x1)
et_1  ALL    PRIMARY       NULL    NULL    NULL        74
      Range checked for each record (index map: 0x1)
et    eq_ref PRIMARY       PRIMARY 15      tt.ActualPC 1

这并不完美, 但要好得多: 这些 rows 值的乘积小了 74 倍. 这个版本在几秒钟内执行.

可以进行第二次更改以消除 tt.AssignedPC = et_1.EMPLOYIDtt.ClientID = do.CUSTNMBR 比较的列长度不匹配:

mysql> ALTER TABLE tt MODIFY AssignedPC VARCHAR(15),
                      MODIFY ClientID   VARCHAR(15);

修改后, EXPLAIN 产生如下所示的输出:

table type   possible_keys key      key_len ref           rows Extra
et    ALL    PRIMARY       NULL     NULL    NULL          74
tt    ref    AssignedPC,   ActualPC 15      et.EMPLOYID   52   Using
             ClientID,                                         where
             ActualPC
et_1  eq_ref PRIMARY       PRIMARY  15      tt.AssignedPC 1
do    eq_ref PRIMARY       PRIMARY  15      tt.ClientID   1

在这一点上, 查询几乎被尽可能地优化了. 剩下的问题是, 默认情况下, MySQL 假定 tt.ActualPC 列中的值是均匀分布的, 而表 tt 并非如此. 幸运的是, 告诉 MySQL 分析 key 分布很容易:

mysql> ANALYZE TABLE tt;

使用附加的索引信息, 连接是完美的并 EXPLAIN 产生以下结果:

table type   possible_keys key     key_len ref           rows Extra
tt    ALL    AssignedPC    NULL    NULL    NULL          3872 Using
             ClientID,                                        where
             ActualPC
et    eq_ref PRIMARY       PRIMARY 15      tt.ActualPC   1
et_1  eq_ref PRIMARY       PRIMARY 15      tt.AssignedPC 1
do    eq_ref PRIMARY       PRIMARY 15      tt.ClientID   1

EXPLAIN 输出中的 rows 列是来自 MySQL 连接优化器的有根据的猜测. rows 通过将产品与查询返回的实际行数进行比较, 检查这些数字是否更接近事实. 如果数字完全不同, 您可能会通过 STRAIGHT_JOINSELECT 语句中使用并尝试在 FROM 子句中以不同的顺序列出表来获得更好的性能.(但是, STRAIGHT_JOIN 可能会阻止使用索引, 因为它禁用了 半连接转换. 请参阅第 8.2.2.1 节 使用半连接转换优化 IN 和 EXISTS 子查询谓词.

在某些情况下, 当 EXPLAIN SELECT 与子查询一起使用时, 可以执行修改数据的语句; 有关更多信息, 请参阅第 13.2.11.8 节 派生表.

扩展 EXPLAIN 输出格式

该 EXPLAIN 语句产生额外的信息, 这些信息不是 EXPLAIN 输出的一部分, 但可以通过在 EXPLAIN 后接着 SHOW WARNINGS 语句来查看. 从 MySQL 8.0.12 开始, 扩展信息可用于 SELECT, DELETE, INSERT, REPLACE 和 UPDATE 语句. 在 8.0.12 之前, 扩展信息仅适用于 SELECT 语句.

SHOW WARNINGS 输出的 Message 显示优化器在 SELECT 语句如何限定表名和列名, SELECT 应用重写和优化规则后的样子, 以及可能有关优化过程的其他注释.

EXPLAIN 后的 SHOW WARNINGS 仅针对 SELECT 语句生成扩展信息. 其他可解释语句 (DELETE, INSERT, REPLACE 和 UPDATE) 则显示的空结果.

这是扩展 EXPLAIN 输出的示例:

mysql> EXPLAIN
       SELECT t1.a, t1.a IN (SELECT t2.a FROM t2) FROM t1\G
*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: t1
         type: index
possible_keys: NULL
          key: PRIMARY
      key_len: 4
          ref: NULL
         rows: 4
     filtered: 100.00
        Extra: Using index
*************************** 2. row ***************************
           id: 2
  select_type: SUBQUERY
        table: t2
         type: index
possible_keys: a
          key: a
      key_len: 5
          ref: NULL
         rows: 3
     filtered: 100.00
        Extra: Using index
2 rows in set, 1 warning (0.00 sec)

mysql> SHOW WARNINGS\G
*************************** 1. row ***************************
  Level: Note
   Code: 1003
Message: /* select#1 */ select ` test` .` t1` .` a` AS ` a` ,
         < in_optimizer> (` test` .` t1` .` a` ,` test` .` t1` .` a` in
         ( < materialize> (/* select#2 */ select ` test` .` t2` .` a`
         from ` test` .` t2` where 1 having 1 ),
         < primary_index_lookup> (` test` .` t1` .` a` in
         < temporary table> on < auto_key>
         where ((` test` .` t1` .` a` = ` materialized-subquery` .` a` ))))) AS ` t1.a
         IN (SELECT t2.a FROM t2)` from ` test` .` t1`
1 row in set (0.00 sec)

因为显示的语句 SHOW WARNINGS 可能包含特殊标记以提供有关查询重写或优化器操作的信息, 所以该语句不一定是有效的 SQL, 并且不打算执行. 输出还可能包含带有 Message 值的行, 这些值提供有关优化器所采取的操作的附加非 SQL 解释性说明.

以下列表描述了可以出现在由 SHOW WARNINGS 显示的扩展输出中的特殊标记:

当某些表属于 constsystem 类型时, 涉及这些表中的列的表达式由优化器提前求值, 而不是显示语句的一部分. 但是, 使用 FORMAT=JSON 时, 某些 const 表访问会显示为使用 const 值的 ref 访问.

获取命名连接的执行计划信息

暂时用不上, 这部分地址: 获取命名连接的执行计划信息

估计查询性能

在大多数情况下, 您可以通过计算磁盘寻道 (disk seeks) 次数来估计查询性能. 对于小型表, 通常可以在一次磁盘查找中找到一行 (因为索引可能已缓存). 对于更大的表, 您可以估计, 使用 B-tree 索引, 您需要这么多次查找才能找到一行:

$$
\frac{log(row\_count)}{log(index\_block\_length / 3 * 2 / (index\_length + data\_pointer\_length))} + 1
$$

在 MySQL 中, 索引块通常为 1024 字节, 数据指针通常为 4 字节. 对于一个 500000 行的表, 键值长度为 3 个字节 (大小为 MEDIUMINT), 公式表示 $log(500000)/log(1024/3*2/(3+4)) + 1= 4$ seeks.

该索引需要大约 $500000 * 7 * 3/2 = 5.2MB$ 的存储空间 (假设典型的索引缓冲区填充率为 2/3), 因此您可能在内存中有很多索引, 因此只需要一两次调用读取数据以查找行.

但是, 对于写入, 您需要四个查找请求来查找放置新索引值的位置, 通常需要两次查找来更新索引并写入行.

前面的讨论并不意味着您的应用程序性能会以 $log(N)$ 的速度缓慢下降. 只要一切都被操作系统或 MySQL 服务器缓存, 随着表变大, 事情只会稍微变慢. 在数据变得太大而无法缓存后, 事情开始变得慢得多, 直到您的应用程序仅受磁盘搜索 (增加 log N) 的约束. 为避免这种情况, 请随着数据的增长而增加 key 的缓存大小. 对于 MyISAM 表, 键缓存大小由 key_buffer_size 系统变量控制. 请参见第 5.1.1 节 配置服务器.

笔者的验证 Demo

没有刻意增加 Explain 的使用 Demo, 后续的开发中会找机会验证的.

原文地址: 【MySQL 文档翻译】理解查询计划

欢迎访问我的博客: http://blog.duhbb.com/

标签:EXPLAIN,查询,索引,文档,key,MySQL,SELECT
来源: https://www.cnblogs.com/tuhooo/p/16463137.html