MySQL 优化 —— EXPLAIN 执行计划详解

引言

本博客大部分内容翻译自MySQL 官网 Understanding the Query Execution Plan 专题。另外有一些补充,则来自于网课以及《高性能MySQL(第三版)》。

根据我们的表、字段、索引、以及 where 子句中的条件等信息,MySQL 优化器会考虑各种技术来更高效地执行查找。一个大表中的查找不一定要读取所有记录;多表连接也不一定需要比较每条联合记录。优化器选择的执行最优查询的操作集,称为“查询执行计划”也可以说是 EXPLAIN 计划。我们的目标就是找到那些可以将查询优化地更好的点,然后通过学习 SQL 语法和索引等技术,来改善执行计划。

一、EXPLAIN 介绍

EXPLAIN 语句提供了 MySQL 如何执行语句的信息:

1、MySQL5.6 之后 EXPLAIN 可以和 SELECT DELETE INSERT REPLACE UPDATE 语句等一起工作;

2、当 EXPLAIN 和一个可解释的语句一起使用时,MySQL 就会展示来自优化器的关于语句执行计划的信息。即,MySQL 会解释它将会怎样执行语句,包括表是如何连接的,以什么方式排序的等信息。

3、When EXPLAIN is used with FOR CONNECTION connection_id  rather than an explainable statement, it displays the execution plan for the statement executing in the named connection.(这句暂不翻译)

4、对于 SELECT 语句, EXPLAIN 提供了额外的执行计划信息,可以用 SHOW WARNINGS 来查看。参考:Section 8.8.3, “Extended EXPLAIN Output Format”.

5、EXPLAIN对于检查涉及分区表的查询非常有用。参考:Section 22.3.5, “Obtaining Information About Partitions”.

6、FORMAT 选项可以用于选择输出格式。TRADITIONAL 以表格的形式展示。如果没有指定 FORMAT 选项,TRADITIONAL 就是默认的。JSON 格式会以 json 格式展示 EXPLAIN 信息。例如:EXPLAIN FORMAT = JSON SELECT... 。

在 EXPLAIN 的帮助下,你可以清楚的知道为了让查询变得更快,该在哪里给表添加索引。你也可以知道优化器是否以最佳的顺序连接各个表。为了让优化器使用 SELECT 语句中表的命名顺序连接各表,以 SELECT  STRAIGHT_JOIN(而不是SELECT)开头即可。(参考:Section 13.2.9, “SELECT Statement”)但是,STRAIGHT_JOIN 可能会妨碍索引的使用,因为它禁用了半连接转换(because it disables semijoin transformations. )。参考:Section 8.2.2.1, “Optimizing Subqueries, Derived Tables, and View References with Semijoin Transformations”.

优化器跟踪(The optimizer trace有时可能提供与 EXPLAIN 互补的信息。但是,优化器跟踪的格式和内容会受不同版本的影响。更多细节,参考:MySQL Internals: Tracing the Optimizer.

如果你对本应该使用索引而没有用到索引的情况感到疑惑,执行一下 ANALYZE TABLE 来更新表统计信息。例如列的基数(cardinality of keys),这会影响优化器做出的选择。参考:Section 13.7.2.1, “ANALYZE TABLE Statement”.

注意:

EXPLAIN 还可以用来获取表的列的信息

EXPLAIN  tb1_name  与  DESCRIBE  tb1_name  、 SHOW  COLUMNS  FROM  tb1_name  是等价的。

更多信息,参考:Section 13.8.1, “DESCRIBE Statement”,和  Section 13.7.5.5, “SHOW COLUMNS Statement”。

二、EXPLAIN 的输出格式

EXPLAIN会为 select 语句中的每张表返回一行信息。并会以MySQL处理语句时读取这些表的顺序罗列它们。

MySQL 解决所有 join 的方法是使用一个“嵌套循环关联”的方法。也就是说,MySQL会从第一张表中读取一条记录,然后找到第二张表中与之匹配的记录,然后再找第三张表,依此类推。当所有的表处理完毕,MySQL会输出查询的列并回溯表列表(table list),直到找到一个有更多行的表(译者注:连接表的时候,主表查询出的记录往往是最多的,从主表开始关联查询,再回溯到主表,可能官网想表达的是这个意思)。下一条记录会从该表中读取,并且继续处理下一张表。

EXPLAIN 输出包含了分区信息(partitions 列)。同样,对于 SELECT 语句,EXPLAIN 会生成扩展信息,只要在 EXPLAIN 执行完成后,直接执行 SHOW WARNINGS 即可。参考:Section 8.8.3, “Extended EXPLAIN Output Format”

注意

旧的MySQL版本中, 分区和扩展信息使用 EXPLAIN PARTITIONS 和 EXPLAIN EXTENDED 输出。这些语法依然向后兼容,但是分区和扩展输出现在默认都是开启的了。所以 PARTITIONS 和 EXTENDED 关键字完全多余。未来版本也会移除的。

不可以在EXPLAIN语句中同时使用 PARTITIONS 和 EXTENDED 关键字。另外,哪一个都不能和 FORMAT 选项一起使用。

MySQL Workbench (译者注:这是一款由 MySQL 官方出品的,类似 Navicat 的数据库管理工具)有一个 Visual Explain 功能,可以提供可视化的 EXPLAIN 输出信息。参考:Tutorial: Using Explain to Improve Query Performance.

2.1 EXPLAIN 输出字段(EXPLAIN Output Columns)

这一节描述了 EXPLAIN 的输出字段。后面的两节则提供了更多的关于 type 和 Extra 字段的信息。

EXPLAIN输出的每一行都对应一张表。下面的表提供了EXPLAIN的输出字段,第一列是字段名称,第二列是当 FORMAT = JSON 时的输出字段名称:

ColumnJSON NameMeaning
idselect_idThe SELECT identifier:查询id
select_typeNoneThe SELECT type:查询类型
tabletable_nameThe table for the output row:对应的表
partitionspartitionsThe matching partitions:匹配的分区
typeaccess_typeThe join type:访问类型
possible_keyspossible_keysThe possible indexes to choose:可能用到的索引
keykeyThe index actually chosen:真正被用到的索引
key_lenkey_lengthThe length of the chosen key:用到的索引长度
refrefThe columns compared to the index:与索引比较的列
rowsrowsEstimate of rows to be examined:大约要检索的行数
filteredfilteredPercentage of rows filtered by table condition:按表条件过滤的行的百分比
ExtraNoneAdditional information:附加信息

1、id(JSON 名:select_id)

SELECT 标识符(SELECT identifier)。这是一个连续的数字,用以标识查询中的 SELECT 。如果引用了其他行的联合结果集(union result of other rows),那么 id 会为 NULL。这种情况下,该行的 table 字段会显示为 <union M, N> 这样的形式,表示该行代表了 id 值为 M 和 N 的行的联合(the row refers to the union of the rows with id values of M and )。

重点:id 是一个自然数编号,如1、2,但有时也可以是NULL。如上所述,NULL的时候,就是引用了一个 UNION 结果集

当 id 为数字的时候,编号大的会先执行。有时候,编号会相同,相同编号就从上到下执行

2、select_type(JSON 名:无)

查询类型。MySQL将查询分为简单和复杂类型,复杂类型可分为三大类简单子查询FROM子查询以及UNION查询。select_type 就是用于区分这三类复杂查询。可选值如下(红色标记为常见值):

select_type ValueJSON NameMeaning
SIMPLENone简单查询(没有任何 UNION 或 子查询)。
PRIMARYNone主查询,如果查询中包含任何复杂的子部分,那么最外层查询被标记PRIMARY
UNIONNoneUNION 中的第二个或后面的SELECT语句
DEPENDENT UNIONdependent (true)UNION 中的第二个或后面的SELECT语句, 依赖于外部查询
UNION RESULTunion_result从UNION 的结果获取数据的SELECT。
SUBQUERYNoneSELECT子句或WHERE子句中的子查询
DEPENDENT SUBQUERYdependent (true)子查询中的第一个 SELECT, 依赖于外层查询
DERIVEDNone派生表。FROM子句中的子查询。MySQL会递归执行这些子查询,把结果放在临时表里
MATERIALIZEDmaterialized_from_subqueryMaterialized subquery  物化子查询。参考《MySQL高级 —— 查询性能优化》4.1节
UNCACHEABLE SUBQUERYcacheable (false)非缓存子查询,结果不能被缓存的子查询,必须被外部查询的每一行重新求得
UNCACHEABLE UNIONcacheable (false)非缓存子查询(uncacheable subquery)的 UNION 中的第二个或后面的 SELECT

SUBQUERY还可以被标记为DEPENDENT SUBQUERY,这一般是指SELECT依赖于外层查询发现的数据(很可能是依赖于FROM派生表的外层SELECT)。参考:Section 13.2.10.7, “Correlated Subqueries” 。

DEPENDENT SUBQUERY 的取值与 UNCACHEABLE SUBQUERY(由于用户变量等原因) 的取值不同。对于 DEPENDENT SUBQUERY ,对于来自其外部查询的变量的每组不同值,子查询只重新计算一次。而对于 UNCACHEABLE SUBQUERY ,对外部查询的每行记录,该子查询都会计算一遍。

子查询缓存与缓存中的查询结果缓存不一样(具体描述参考 Section 8.10.3.1, “How the Query Cache Operates”)。子查询缓存发生在查询执行过程中,而查询结果缓存只在查询执行完毕时才会存储结果。

当你在 EXPLAIN 语句中指定了 FORMAT = JSON ,输出的结果并没有一个对应 select_type 的单独属性;query_block 属性对应给定的 SELECT 。与刚才显示的大多数 SELECT 子查询类型等价的属性都是有的,并且在合适的时机就会展示。不过并没有与 SIMPLE 和 PRIMARY 等价的 JSON 值。

select_type 属性值对于非 SELECT 语句,会展示影响表的语句类型如 DELETE 语句的 select_type 就是 DELETE

3、table(JSON 名:table_name)

explain 输出的每一行都对应一个表别名或表名。它可以是下面的值中的一个:

<union M, N> : 这一行引用了 id 值为 M 和 N 的表的联合。

<derived N> : 这一行引用了 id 值为 N 的表所派生的表。派生的表可能是一个结果集,比如,FROM 子句中的子查询。

<subquery N> : 这一行引用了 id 值为 N 的物化子查询的结果。参考:Section 8.2.2.2, “Optimizing Subqueries with Materialization”.

4、partitions(JSON 名:partitions)

查询的记录将会在哪个分区中匹配。NULL 代表没有分区表。参考: Section 22.3.5, “Obtaining Information About Partitions”.

5、type(JSON 名:access_type)

关联类型,但更准确的说法是——访问类型,换言之就是MySQL决定如何查找表中的行。参考 2.2 节。

6、possible_keys(JSON 名:possible_keys)

该属性可以表明查询中,对应表有哪些索引可以使用。注意这个属性完全不依赖于表在 explain 输出中的显示顺序。也就是说,以生成的表顺序 ,possible_keys 中的有些索引可能实际中并不会用到。

如果该属性是 NULL (或者在 JSON 格式中是 undefined ),代表没有相关的索引。这时,你可能就应该努力通过调试 WHERE 子句来提升你的查询性能,检查是否涉及到了一些字段或者适合索引查询的字段。如果有,就创建一个合适的索引,然后再次通过 EXPLAIN 进行检验。

查看一个表有哪些索引,可以使用 SHOW INDEX FROM tbl_name 语句。

7、key(JSON 名:key)

这一列表示 MySQL 决定采用哪个索引来优化对该表的访问。如果 MySQL 决定使用 possible_keys 中的一个索引去查找记录,那么这个索引就会列在 key 属性中。

key 中也会出现 possible_key 中没有出现的索引。发生这种情况,很可能是 possible_keys 没有找到适合查询的索引,但是所有查询的字段都在索引中。也就是说,查询使用了覆盖索引。因此,尽管它不用于决定要查询哪些行,但却依然可以用于查询字段,因为索引扫描依然比行扫描更高效。换句话说,possible_keys 揭示了哪一个索引能有助于高效地行查找,而 key 显示的是优化采用哪一个索引可以最小化查询成本

对于InnoDB ,即使查询列表中有主键,二级索引也可能覆盖所查询的字段,因为InnoDB用每个二级索引存储了主键值。如果列是NULL, MySQL就找不到索引来更有效地执行查询。

要强制MySQL使用或忽略在 possiblele_keys 中列出的索引,请在查询中使用 FORCE INDEXUSE INDEX 或 IGNORE INDEX 。参考: Section 8.9.4, “Index Hints”.

对于 MyISAM,运行 ANALYZE TABLE 可以帮助优化器选择更好的索引。对于 MyISAM 表来说, myisamchk --analyze 也是一样的。参考: Section 13.7.2.1, “ANALYZE TABLE Statement”, 和 Section 7.6, “MyISAM Table Maintenance and Crash Recovery”.

8、key_len(JSON 名:key_length)

该字段表示 MySQL 在索引里使用的字节数。

因为key_len是通过查找表的定义而被计算出,而不是表中的数据,因此它显示了在索引字段中可能的最大长度,而不是表中数据使用的实际字节数。key_len 的值可以让你判断 MySQL 究竟用到了复合索引的哪几个索引列。如果 key 属性的值为 NULL , 那么 key_len 肯定也是 NULL 。

由于索引的存储格式,那些可以为 NULL 的字段的索引长度要比非空字段的索引长度大一些。

MySQL并不总是显示一个索引真正使用了多少。例如,如果对一个前缀模式匹配(例如 '张%')执行LIKE查询,它会显示列的完整宽度正在被使用。

计算 key_len 的简易方法:

int 类型在MySQL中以4个字节存储,key_len 为 4,如果列值允许为 NULL,那么需要 + 1,即 key_len 为 5.

double 类型以8个字节存储,key_len 为 8,如果允许 NULL,那么同样 +1, 即 key_len 为 9.

char(n) 定长字符串,首先需要看字符集,常见的utf8以3个字节存储每个字符,gbk用2个,latin用1个。key_len 就等于每个字节长度乘以允许最大字符数n,如果允许NULL,key_len 也要 +1。例如 char(20) DEFAULT NULL,编码为utf8 ,那么 key_len 就是 3 × 20 + 1 = 61。如果不允许为 NULL ,就是60。

varchar(n)变长字符串,每个字符:utf8为3字节、gbk为2字节、latin为1字节。由于是变长,因此 key_len 要 +2,如果允许 NULL,同样 +1。其他和 char计算方式一样。例如,varchar(20) DEFAULT NULL,编码 utf8,那么 key_len 就是:

3 × 20 + 2 + 1 = 63,如果不允许为 NULL,就是62。

上面的说明只是单独计算每种列值类型的方法,如果是复合索引,那么key_len 就是用到的索引列长度和。

9、ref(JSON 名:ref)

ref 列显示了常量或哪些列与 key 列中的索引进行了比较。只有 type 列是 ref 的时候,ref 列才会有值。

简单的说,就是 key 中的索引,如果与一个常量比较,那么 ref 会显示 const,如果是与其他表的某个列进行比较,那么就会显示该列名。

如果 ref 属性的值是 func ,那么用到的值就是某些函数的结果。想要知道是哪个函数,在 EXPLAIN 执行后使用 SHOW WARNINGS ,查看EXPLAIN 的扩展信息。

函数实际上可能是一个运算符,比如算术运算符。

10、rows(JSON 名:rows)

rows 列表示MySQL认为执行查询必须检查的行数。这个数字是内嵌关联循环计划里的循环数目。也就是说,它不是最终的结果集里的行数,而是MySQL为了找到符合条件的结果集而必须读取的行的平均数。

对于 InnoDB 表,这个数是一个估值,而且可能并不总是准确的。

11、filtered(JSON 名:filtered)

filtered 属性表示被筛选条件过滤掉的记录条数占全表的估计百分比。最大值是100,意味着记录全部被过滤掉。从100开始递减的值表示过滤的量在增加。rows 属性表示了需要检查的估计行数,rows 乘 filtered 表示了将会被后面的表关联的记录条数。例如,如果 rows 是1000,filtered 是 50.00(50%),那么要与后面的表连接的记录条数就是 1000 × 50% = 500。

对于filtered ,原文的描述是:The filtered column indicates an estimated percentage of table rows that will be filtered by the table condition. The maximum value is 100, which means no filtering of rows occurred.  这里面有一个语义上的陷阱,即 filtered 究竟表示的是 “被过滤掉的” ?还是 “过滤后(留下来)的” ,经过本人测试,filtered 表示的是前者,即 “被过滤掉的” ,这样后面的语义也就基本自洽了。而 filtering 则表示 “过滤后(留下来)的” 。

12、Extra(JSON 名:none)

这一列显示了关于 MySQL如何处理查询的额外信息。对于不同值的描述,参考:Extra Information. 或参考下面 2.3 节。

2.2 EXPLAIN type访问类型(EXPLAIN Join Types)

type 属性描述了表之间是如何连接(或关联)的。在 JSON 格式输出中,对应 access_type 属性。下面的列表描述了访问类型,顺序从“最理想类型”到“最糟糕的类型”:

system > const > eq_ref > ref > range > index > ALL

2.2.1 system(不常见)

表只有一行(=系统表)。是 const 连接类型的一种特殊情况。

2.2.2 const

表最多只有 1 条匹配记录,在查询开始时就会读取该表。因为只有一行,所以这一行中列的值可以被其他优化器视为常量。const 访问类型非常快,因为他们只会被读取一次。MySQL能将这个查询转换为一个常量,然后可以高效地将表从连接操作中移除。

const 会在你使用整个主键(all parts of a PRIMARY KEY)唯一索引(UNIQUE index)去比较一个常量的时候用到。在下面的查询中,tb1_name 就是一张 const 表:

SELECT * FROM tbl_name WHERE primary_key=1;SELECT * FROM tbl_nameWHERE primary_key_part1=1 AND primary_key_part2=2;

2.2.3 eq_ref

使用这种索引查找,MySQL知道最多只返回一条符合条件的记录。它会在所有的索引部分都被用到的时候以及索引是主键非空唯一索引时出现到,它会将它们与某个参考值做比较。MySQL 对于这类访问类型的优化做的非常好,因为MySQL知道无须估计匹配行的范围或在找到匹配行后再继续查找

eq_ref 会在索引列使用 = 号的时候用到。比较的值可以是一个常量也可以是一个从前表读取的列(的表达式)。在下面的例子中,MySQL 可以使用 eq_ref 类型来处理 ref_table:

SELECT * FROM ref_table,other_tableWHERE ref_table.key_column=other_table.column;SELECT * FROM ref_table,other_tableWHERE ref_table.key_column_part1=other_table.columnAND ref_table.key_column_part2=1;

2.2.4 ref

这是一种索引访问(有时也叫“索引查找”)它返回所有匹配某个单个值的行,是查找和扫描的混合体。此类索引访问只有当使用非唯一性索引唯一性索引的非唯一性前缀时才会发生。把它叫做 ref 是因为索引要跟某个参考值相比较。这个参考值可以是一个常数,或是来自多表查询的结果值。如果该筛选列可以匹配少量的记录,那 ref 还算是一个不错的连接类型。

ref_or_null 是ref 之上的一个变体,它意味着MySQL必须在初次查找的结果里进行第二次查找以找出NULL条目。

ref 也可以在索引列使用 = 或 <=> 号的时候被用到。下面的例子,MySQL 可以使用 ref 来处理 ref_table:

SELECT * FROM ref_table WHERE key_column=expr;SELECT * FROM ref_table,other_tableWHERE ref_table.key_column=other_table.column;SELECT * FROM ref_table,other_tableWHERE ref_table.key_column_part1=other_table.columnAND ref_table.key_column_part2=1;

2.2.5 full_text(不常见)

这种连接方式会在使用 FULLTEXT 索引的时候用到。

2.2.6 ref_or_null(不常见)

这种连接方式和 ref 类似,除此之外, MySQL 还会额外搜索包含 NULL 值的记录。这种连接类型的优化绝大多数是在处理子查询的时候。在下面的例子中, MySQL 会使用 ref_or_null 来处理 ref_table:

SELECT * FROM ref_tableWHERE key_column=expr OR key_column IS NULL;

参考:Section 8.2.1.13, “IS NULL Optimization”. 

2.2.7 index_merge(不常见)

这种连接类型表示使用了索引合并优化(Index Merge optimization)。这种情况下,explain 中的 key 属性会罗列出被用到的索引,key_len 属性会列出用到的索引的最长的索引部分。参考:Section 8.2.1.3, “Index Merge Optimization”.

2.2.8 unique_subquery(不常见)

这种类型在类似下面的一些使用 IN 的子查询时取代了 eq_ref:

value IN (SELECT primary_key FROM single_table WHERE some_expr)

 unique_subquery 只是一个索引查找函数,它完全取代了子查询,以提高效率。

2.2.9 index_subquery(不常见)

这种连接类型有点像 unique_subquery 。它取代了 IN 子查询,但它只在子查询中有非唯一索引时才会起作用,类似下面这样:

value IN (SELECT key_column FROM single_table WHERE some_expr)

2.2.10 range 

这种连接类型会使用索引查询给定范围内的记录。EXPLAIN 输出中的 key 属性表示了哪个 索引列 被用到。key_len 包含了被用到的最长的索引部分。ref 属性为 NULL。

range 类型会在索引列使用 =、<>、>、>=、<、<=、IS NULL、<=>、BETWEEN、LIKE、或 IN() 任意一种操作符去比较常量的时候被用到。当使用 IN或 OR 列表的时候,显示的范围扫描,其实并不能和 > 这类比较符的性能等同,虽然它们在EXPLAIN中显示的类型都是 range,但是 IN() 列表其实属于等值列表。参考《MySQL高级 —— 高性能索引》6.2 节。

SELECT * FROM tbl_nameWHERE key_column = 10;SELECT * FROM tbl_nameWHERE key_column BETWEEN 10 and 20;SELECT * FROM tbl_nameWHERE key_column IN (10,20,30);SELECT * FROM tbl_nameWHERE key_part1 = 10 AND key_part2 IN (10,20,30);

2.2.11 index

index 类型除了会扫描索引树之外,其他和 ALL 是一样的。会有两种情况出现:

1、如果索引是一个覆盖索引,那么这种类型的查询就只会扫描索引树。这种情况下, Extra 属性会显示 Using Index。一个只扫描索引的方式比 ALL 更快,这是因为索引数据肯定要比表中数据要少。

2、以索引次序扫描全表。Extra 不会显示 Uses Index。

index 类型的主要优点是避免了排序,最大缺点是要承担按索引次序读取整个表的开销。

MySQL 会在查询只用到了单一索引列的时候用到 index 这种类型。

2.2.12 ALL

这就是人们常说的“全表扫描”,这种类型会对前面各表的组合记录都进行全表扫描。如果表是第一个没有被标记为 const 的表,这通常是不好的,在所有其他情况下通常是非常糟糕的。通常你可以通过增加索引来避免 ALL 。但也有例外,例如在查询中使用了 LIMIT,或在 Extra 列中显示“Using distinct/not exists”。

2.3 EXPLAIN Extra 信息(EXPLAIN Extra Information)

Extra 属性显示了MySQL如何执行查询的额外信息。

2.3.1 Using index

此值表示MySQL将使用覆盖索引,以避免访问表。不要把覆盖索引和 type = index 访问类型混淆了。

2.3.2 Using where

这意味着MySQL服务器将在存储引擎检索行后再进行过滤。当它读取索引时,就能被存储引擎检验,因此不是所有带有 WHERE子句的查询都会显示“Using where” 。有时“Using where” 的出现就是一个暗示:查询可受益于不同的索引。

2.3.3 Using temporary

这意味着MySQL在对查询结果排序时会使用一个临时表。

2.3.4 Using filesort

这意味着MySQL会对结果使用一个外部索引排序,而不是按照索引次序从表里读取行。MySQL有两种文件排序算法,两种方式都可以在内存或磁盘上完成。EXPLAIN 不会告诉你 MySQL将使用哪一种文件排序,也不会告诉你排序会在内存里还是在磁盘上完成。

2.3.5 Range checked for each record (index map:N)

这个值意味着没有好用的索引,新的索引将在连接的每一行上重新估算。N是显示在possible_keys 列中索引的位图,并且是冗余的。

 

2.4 EXPLAIN 输出的解释

EXPLAIN输出可以给你在连接各种表查询的时候一个非常好的指示作用。这会大致告诉你MySQL 在执行查询的时候必须要检查多少行记录。如果你限制了 max_join_size 系统变量,那么 EXPLAIN 也会被用来告诉我们一些有用的东西。参考: Section 5.1.1, “Configuring the Server”.

下面的例子显示了多表连接是如何基于 EXPLAIN 提供的信息一点点优化的

假设你有一个查询语句,并且你通过 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.CUSTNAMEFROM tt, et, et AS et_1, doWHERE tt.SubmitTime IS NULLAND tt.ActualPC = et.EMPLOYIDAND tt.AssignedPC = et_1.EMPLOYIDAND tt.ClientID = do.CUSTNMBR;

对于这个例子,做出下面的假设:

1、比较的列(译者注:columns being compared,实际上指的就是where 子句后面作为筛选条件的列,因为往往需要用到 = 号等操作符,因此在官网中一般都被称为被比较的列)定义如下:

TableColumnData Type
ttActualPCCHAR(10)
ttAssignedPCCHAR(10)
ttClientIDCHAR(10)
etEMPLOYIDCHAR(15)
doCUSTNMBRCHAR(15)

2、表有以下这些索引:

TableIndex
ttActualPC
ttAssignedPC
ttClientID
etEMPLOYID (primary key)
doCUSTNMBR (primary key)

3、tt 表的 ActualPC 字段不是均匀分布的。

首先,在所有优化执行之前, 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 3872ClientID,ActualPCRange checked for each record (index map: 0x23)

因为每张表的连接类型都是 ALL ,这表明MySQL 正在生成一张笛卡尔集(a Cartesian product),也就是表中的每一行都进行了组合。这会花费相当长的时间,因为必须检查每个表中行数的乘积。对于这个案例,乘积就是: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.ActualPC 和 et.EMPLOYID 都是 VARCHAR(15) 了。再次执行 EXPLAIN 就会得到下面的结果:

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

这依然不够完美,但是也稍微好了点:rows 的乘积少了 74 倍(译者注:et 表的 rows 由 74 变为了 1)。这一版的执行会在几秒钟完成。

第二处修改可以针对 tt.AssignedPC = et_1.EMPLOYID 和 tt.ClientID = do.SUTNMBR 这两个比较中有关列长度不匹配的问题。

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   UsingClientID,                                         whereActualPC
et_1  eq_ref PRIMARY       PRIMARY  15      tt.AssignedPC 1
do    eq_ref PRIMARY       PRIMARY  15      tt.ClientID   1

此时,查询几乎已经优化的足够好了。遗留的问题是,默认情况下,MySQL 假设 tt.ActualPC 字段上的值是均匀分布的,但 tt 表并不是这样的(前面的假设)。幸运的是,要告诉 MySQL 分析列值分布情况是非常简单的,你只需要这样做:

mysql> ANALYZE TABLE tt;

凭借额外的索引信息,连接查询已经变得完美,EXPLAIN 也变成了如下结果:

table type   possible_keys key     key_len ref           rows Extra
tt    ALL    AssignedPC    NULL    NULL    NULL          3872 UsingClientID,                                        whereActualPC
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 的乘积与查询返回的实际行数进行比较,就可以检查这些数字是否接近实际情况。如果数字与实际查询的行数相差甚远,你可以通过在你的 SELECT 语句中使用 STRAIGHT_JOIN 并尝试在 FROM 子句中以不同的顺序罗列所查各表来获取更好的性能。(但是,STRAIGHT_JOIN 可能会妨碍到索引的使用,因为它禁用了半连接转换。参考:Section 8.2.2.1, “Optimizing Subqueries, Derived Tables, and View References with Semijoin Transformations”.)

在某些情况下,当EXPLAIN SELECT与子查询一起使用时,可以执行修改数据的语句。参考:Section 13.2.10.8, “Derived Tables”.

总结

这篇译文翻译了很长时间,断断续续可能有一个月。本篇文章有些地方可能翻译的并不准确,因此希望各位可以与原文比较阅读,增加理解。

另外,本来想在 Extra 部分就结束本篇翻译,没想到 MySQL 官网在最后一节给出了一个非常亲民的案例讲解,可以让我们一览 EXPLAIN 的常规用法。这一部分也是我认为翻译的比较准确的部分。

因为 EXPLAIN 语句非常重要,因此,这篇译文我也会经常翻阅,加深理解的同时不断纠正文中翻译的不准确或有所偏颇之处,同时希望大家能给予意见或建议。

2020-05-29 追加的部分,分散在文章的各个小节中,主要是在读完《高性能MySQL(第三版)》的五六章,以及附录EXPLAIN的部分,对执行计划和一些索引的概念有了更进一步的理解和认识。之前翻译的不是很准确的地方做了校对和润色,某些废话也是能删就删,我还写了很多关于索引及查询优化相关的文章,可以和这些文章一起阅读,结合实践并反复回看的话,相信一定可以成为MySQL性能优化领域的好手。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/558707.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Git 初学札记(十)—— Reset 回退的三种状态解析

引言 工作中经常会涉及到需要本地代码覆盖更新的操作。有时候可能是从远端git 上直接覆盖更新&#xff0c;或者是其他本地分支覆盖更新当前分支等等。这个时候就需要用到 reset 操作。 reset 操作分为三种类型&#xff1a;Soft、Mixed、Hard。今天我们就来说说这三种类型究竟…

MySQL 高级 —— 深入理解 InnoDB 与 MyISAM

引言 在文件系统中&#xff0c;MySQL将每个数据库&#xff08;也可以称之为schema&#xff09;保存为数据目录下的一个子目录。创建表时&#xff0c;MySQL会在数据库子目录下创建一个与表同名的.frm文件保存表的定义。因为MySQL使用文件系统的目录和文件来保存数据库和表的定义…

Ts声明ElementUI控件

初用Ts&#xff0c;有时候想获取三方控件不太会声明类型&#xff0c;记录一下使用InstanceType导入类型 例如声明一个el-select <el-form-item label"类型:" prop"year" :loading"state.loading"><el-select v-model"props.ruleF…

关于 OutOfMemoryError 的总结与解决方法

引言 本文总结自周志明的《深入理解Java虚拟机》第二章部分内容。 这部分内容&#xff0c;可以为后续性能调优方面的工作起到铺垫作用。 一、什么是 OutOfMemoryError OurOfMemory 简称“OOM”&#xff0c; 直译为“内存耗尽”或“内存溢出”&#xff0c;当然&#xff0c;并…

Windows误关闭资源管理器重启的办法

引言 有时候Windows系统在开机后&#xff0c;在桌面底部的任务栏中无法正常加载必要的网络连接图标或音量图标等&#xff0c;导致无法手动操作音量或连接网络。这时候就会需要打开“任务管理器”重新启动“资源管理器”使其重新加载这些必要的控制图标。 但是由于操作失误&am…

MySQL高级 —— 高性能索引

引言 最近一直在抱着《高性能MySQL&#xff08;第三版&#xff09;》研究MySQL相关热点问题&#xff0c;诸如索引、查询优化等&#xff0c;这阶段的学习是前一段时间MySQL基础与官方的“阅读理解”的进一步延伸。 书中第五章详细阐述了如何设计高性能的索引&#xff0c;以及索…

MySQL高级 —— 查询性能优化

引言 承接《MySQL高级 —— 高性能索引》&#xff0c;本篇博客将围绕《高性能MySQL&#xff08;第三版&#xff09;》第六章内容进行总结和概括。 与索引的部分一样&#xff0c;SQL优化也是广大程序员深入MySQL的又一条必经之路。希望通过本篇博客的总结&#xff0c;能够为我…

哈希表的大小为何最好是素数

引言 为什么散列函数采用取模运算&#xff1f;又为什么取模运算的被取模数最好是素数&#xff1f;素数是如何在取模运算中很好的规避冲突的&#xff1f; 这些问题可能困扰诸多程序员很久了。我们总是说素数可以更好的避免冲突&#xff0c;但总是对各种长篇大论的分析望而却步…

Java常用设计模式————适配器模式

引言 由于无法直接使用某个类中的方法而采取的一种中间类转换的策略。将一个类的接口转换成另一个接口&#xff0c;让原本接口不兼容的类可以兼容。 适配器模式可以分为三种&#xff1a;类适配器、对象适配器、接口适配器。它们之间的区别主要体现在适配器角色与被适配角色之…

Java常用设计模式————桥接模式

引言 在实际的业务中&#xff0c;经常会遇到多维度的概念组合&#xff0c;公园的门票&#xff0c;颐和园有年票、月票、日票&#xff0c;故宫也有年票、月票、日票。那么不同的公园和票种类型就可以视为两种不同的纬度&#xff0c;它们之间会形成相互组合的关系。 在类的设计…

Java常用设计模式————装饰者模式

引言 装饰者模式&#xff0c;又叫装饰器模式。它可以动态的将新功能附加到对象上。在对象功能扩展方面&#xff0c;它比继承更灵活&#xff0c;同时装饰者模式也体现了OCP原则。 在客户端调用使用了装饰者模式的对象时&#xff0c;就好像在使用构造器层层包裹核心对象&#x…

Java常用设计模式————组合模式

引言 组合模式&#xff0c;是一种类似递归算法的结构性设计模式&#xff0c;通过以简单的 List &#xff0c;组合本类对象&#xff0c;实现树状对象结构的“部分、整体”的层次。 它可以让调用程序不需要关心复杂对象与简单对象的区别&#xff0c;而统一地实现处理逻辑。 对…

MySQL 高级 —— MVCC 多版本并发控制

引言 MySQL的大多数事务型存储引擎实现的都不是简单的行级锁。基于提升并发性能的考虑&#xff0c;它们一般都同时实现了多版本并发控制——MVCC。包括其他数据库如Oracle等&#xff0c;由于MVCC并没有一个统一的实现标准&#xff0c;因此它们的实现原理都不尽相同。 MVCC简介…

Java常用设计模式————外观模式

引言 外观模式&#xff08;Facade Pattern&#xff09;&#xff0c;又叫“过程模式”。外观模式为子系统中的一组接口提供一个一致的入口&#xff0c;此模式定义了一个高层接口&#xff0c;这个接口使得这一组子系统更加易用。 一、案例分析 生活中有很多类似的案例&#xf…

Java常用设计模式————享元模式

引言 享元模式&#xff0c;也叫蝇量模式&#xff08;Flyweight Pattern&#xff09;。运用共享技术有效地支持大量细粒度的对象。 享元模式常用于系统底层开发&#xff0c;解决系统的性能问题。例如数据库连接池&#xff0c;里面都是创建好的连接对象&#xff0c;在这些连接对…

IDEA——常用基础设置

一、设置入口 File—>Settings... 或者 在工具栏的“小扳手”图标。 二、主题设置 三、编辑通用设置 设置面板中的 Editor 3.1 自动导包 可以设置IDEA自动为程序导包&#xff0c;在书写时加入准确的导包&#xff0c;在书写时优化导包&#xff08;自动去掉未使用的&#…

IDEA——常用快捷键

引言 总结 IDEA 的常用快捷键&#xff0c;除了部分快捷键与 Eclipse 保持一致之外&#xff0c;枚举更多的实用快捷键。 一、如何设置快捷键 在 Settings -> Keymap 中&#xff0c;下拉框里选择 Eclipse &#xff0c;即可将 IDEA 的快捷键设置为与 Eclipse 保持一致。但并…

IDEA——常用代码模板

引言 IDEA 提供了一些内置的代码模板&#xff0c;可以让开发者快速方便的使用&#xff0c;当然 eclipse 中也是有的&#xff0c;比如输入 syso 快速生成输出语句&#xff0c;main 快速生成主函数等。 idea 的模板设置都在 Settings --> Live Templates 和 General-->Po…

IDEA——Git 的设置与使用

引言 在本机下载好 Git 之后&#xff0c;再去在 IDEA 中设置 Git 相关的参数。详细的 Git 操作和 Eclipse 大同小异&#xff0c;可以移步至&#xff1a;《Git必知必会》 一、设置Git执行程序路径 二、导入一个新的远程 git 托管项目 打开 File ——> New ——> Project…

IDEA——Maven的配置与使用

引言 简单介绍一下如何在 idea 中配置maven&#xff0c;以及如何去使用 maven 。 一、配置 Maven home Maven home 和 settings 文件一般都需要进行重新设置&#xff0c;关联到本机已经安装好的 maven 版本&#xff0c;settings 这里可以使用默认&#xff0c;也可以设置为 ma…