
探索 MySQL 索引优化神器
原文链接(请科学上网):https://betterprogramming.pub/understand-the-mysql-index-optimization-artifact-d4d7c6eb31f3
随着用户数量和数据量的增长,慢查询可能是一个无法回避的问题。一般来说,如果出现慢查询,都会伴随着出现接口响应慢,接口超时等问题。
如果是在高并发场景,可能会导致数据库连接被打满,直接导致服务不可用。
慢查询会引起很多问题。那么我们该如何优化慢查询呢?
主要的解决方式有如下一些:
- 监控执行的
SQL,发送邮件和手机短信告警,方便快速定位慢查询SQL; - 开启数据库慢查询日志功能;
- 简化业务逻辑;
- 代码重构和优化;
- 异步处理;
SQL优化;- 索引优化。
这篇文章我主要会关注索引优化,因为索引优化是解决慢查询 SQL 问题最有效的一种方式。
如何查看 SQL 索引的执行状态?
是的,通过在 SQL 语句前面添加 explain 关键字,我们可以查看 SQL 的执行计划。通过执行计划,我们可以清晰地看到表和索引的执行情况,索引是否使用,索引执行的顺序,使用索引的类型等等。
优化索引的步骤如下:
- 使用
explain查看SQL执行计划; - 确定哪些索引使用不当;
- 优化
SQL,可能需要多次优化SQL才能达到索引使用的最佳效果。
Explain 是什么
我们来看看 MySQL 的官方文档是怎么描述 explain 的:

explain 语法
1 | {EXPLAIN | DESCRIBE | DESC} |
用一个简单的 SQL 看看使用 explain 关键字的效果:
1 | explain select * from test1; |

从上图可以看出,执行结果中会显示 12 列信息。
每个列具体信息如下:

说白了,我们需要了解这些列的具体含义,才能正常判断索引的使用情况。事不宜迟,让我们马上开始。
id 列
该列的值为 select 查询中的序号,如 1、2、3、4 等,决定了表的执行顺序。
一条 SQL 的执行计划一般有三种情况:
- 相同
id; - 不同
id; - 相同
id和不同id同时出现。
那么,在这三个 case 中表的执行顺序是怎样的呢?
1. 相同 id
1 | explain select * from test1 t1 inner join test1 t2 on t1.id=t2.id |

我们可以看到执行结果中的两条数据 id 是相同的,都是 1。
在这个场景中表的执行顺序是什么样的呢?
答案:从上到下开始执行,首先执行表 t1,接着执行表 t2。
2. 不同 id
1 | explain select * from test1 t1 where t1.id = (select id from test1 t2 where t2.id=2); |

我们可以看到执行结果中的两条数据 id 是不同的,第一条数据 1,第二条数据是 2。
在这个场景中表的执行顺序是什么样的呢?
答案:序号大的会首先被执行。在这里将会从下到上开始执行,表 t2 将首先被执行,接着表 t1 将被执行。
3. 相同 id 和 不同 id 同时出现
1 | explain |

我们在执行结果中看到了三条数据。前两条数据 id 相同,第三条数据 id 与前一条不同。
在这个场景中表的执行顺序是什么样的呢?
答案:先执行序号大的,从下往上执行。当序号相同时,从上往下执行。因此,此列中表的顺序是 test1、t1。
注意:有一个特殊的表名称,内容为 <derived2>,表示是派生表,文章后面会详细介绍。
select_type 列
这一列表示 select 的类型,具体包括以下 11 种类型:
SIMPLE:简单查询;PRIMARY:最外层查询;UNION:UNION之后的第二个或以后的查询;DEPENDENT UNION:UNION之后的第二个或后面的查询,取决于外部查询;UNION RESULT:UNION的结果;SUBQUERY:第一个子查询;DEPENDENT SUBQUERY:第一个子查询,取决于外部查询;DERIVED:派生表;MATERIALIZED:物化子查询;UNCACHEABLE SUBQUERY:结果无法缓存的子查询;UNCACHEABLE UNION:无法缓存结果的UNION之后的第二个查询或后面的查询。
最常用的有以下几种类型。
SIMPLE:简单的SELECT查询,不包含子查询和UNION操作;PRIMARY:复杂查询中最外层的查询,代表主查询;SUBQUERY:包含在SELECT或WHERE列表中的子查询;DERIVED:FROM列表中包含的子查询,即派生的;UNION:UNION关键字之后的查询;UNION RESULT:UNION操作之后从表中获取结果集。
让我们看一下这些 SELECT 类型是如何出现的?
1. SIMPLE
1 | explain select * from test1; |

它只出现在简单的 SELECT 查询中,不包含子查询和 UNION 操作,这种类型比较直观,就不多说了。
2. PRIMARY 和 SUBQUERY
1 | explain select * from test1 t1 where t1.id = (select id from test1 t2 where t2.id=2); |

我们看到在这个嵌套查询的 SQL 中,最外层的 t1 表是 PRIMARY 类型,最里面的子查询 t2 表是 SUBQUERY 类型。
3. DERIVED
1 | explain |

最后一条记录是派生表,一般是 FROM 列表中包含的子查询,这里是 SQL 语句中的分组子查询。
4. UNION and UNION RESULT
1 | explain |

表 test2 是 UNION 关键字之后的查询,所以它被标识为 UNION,表 test1 是主表,被标识为 PRIMARY。而 <union1,2> 表示 id=1 和 id=2 的表并集,结果被标记为 UNION RESULT。
所以 UNION 和 UNION RESULT 通常是成对出现的。
table 列
该列的值表示输出行所引用的表名,如前面的:test1、test2 等。
但它也可以是以下值之一:
<unionM,N>:M和N并集操作的行记录和记录id;<derivedN>:用于与此行关联的派生表结果id的值N。派生表可能来自(例如)FROM子句中的子查询;<subqueryN>:子查询的结果,其id值为N。
partitions 列
此列的值表示匹配查询记录结果的分区。
type 列
该列的值表示连接类型,是索引执行情况的重要指标。
这包含以下类型:

执行结果从最好到最差的顺序是从上到下。
我们需要关注以下类型:
1 | system > const > eq_ref > ref > range > index > all |
1 | test2 table structure |
在 code 字段上建立一个普通索引。

下面我们一一看看几种常见的连接类型是如何出现的。
1. System
这种类型只需要数据库表中的一条数据,是 const 类型的特例,一般不会出现。
2. Const
通过一个索引可以找到数据,一般用在以主键或唯一索引为条件的查询 SQL 语句中。
1 | explain select * from test2 where id=1; |

3. Eq_ref
通常用于主键或唯一索引扫描。
1 | explain select * from test2 t1 inner join test2 t2 on t1.id=t2.id; |

const 和 eq_ref 都是对主键或唯一索引的扫描,那这两种类型有什么区别?
答案:const 只会被索引一次,eq_ref 的主键与数据记录的主键匹配。由于表中有多条数据,一般情况下,需要对数据进行多次索引才能全部匹配。
4. Ref
常用于非主键索引和唯一索引扫描。
1 | explain select * from test2 where code = '001'; |

5. Range
通常用于范围查询,例如:between...and 或者是 in 操作。
1 | explain select * from test2 where id between 1 and 2; |

6. Index
全索引扫描。
1 | explain select code from test2; |

7. All
全表扫描。
1 | explain select * from test2; |

possible_keys 列
此列表示可能被选择使用的索引。
请注意,此列完全独立于表顺序,这意味着在实际中 possible_keys 列显示的某些索引可能与生成的表顺序并不完全一致。

如果此列结果为 NULL,则表示没有关联索引,在这种情况下,我们可以通过检查 WHERE 子句,查看是否引用了一些符合索引条件的列来提高查询性能。
Key 列
此列表示实际使用的索引。有可能会出现 possible_keys 列为空,但是 key 列不为空的情况。
1 | test1 table structure |
code 和 name 列创建了联合索引。

1 | explain select code from test1; |

这条 SQL 预计不会使用索引,但实际上使用了全索引扫描。
key_len 列
此列表示被使用到的索引的长度。上面的 key 列可以看出索引是否被使用,key_len 列可以进一步看出索引是否被充分利用,毫无疑问,它是非常重要的列。

key_len 是如何计算的呢?
有三个因素决定了 key_len 的结果:
- 字符集(
Character set) - 字段长度(
Length) - 是否为空(
Is it empty)
常用字符编码占用的字节数如下:
GBK:2字节;UTF8:3字节;ISO8859–1:1字节;GB2312:2字节;UTF-16:2字节。
MySQL 一些常用字段类型占用的字节数:
char(n):n字节;varchar(n):n + 2字节;tinyint:1字节;smallint:2字节;int:4字节;bigint:8字节;date:3字节;timestamp:4字节;datetime:8字节。
另外,如果字段类型允许为空,则添加一个字节。
上图中 184 的值是怎么计算出来的?
首先,我使用的数据库的字符编码格式:UTF8,占三个字节。
1 | 184 = 30 * 3 + 2 + 30 * 3 + 2 |
然后,把 test1 表的 code 字段类型改成 char,改成允许为空,再测试。
1 | explain select code from test1; |

1 | 183 = 30 * 3 + 1 + 30 * 3 + 2 |
还有一个问题:为什么这一列可以显示索引是否被完全使用?
1 | explain select code from test1 where code='001'; |

上图中使用了联合索引:idx_code_name。如果索引匹配所有的 key_len,应该是 183,但实际上是 92,也就是说没有使用到所有的索引,索引没有被完全使用。
ref 列
此列表示索引命中的列或常量。
1 | explain select * from test1 t1 inner join test1 t2 on t1.id=t2.id where t1.code='001'; |

我们看到表 t1 命中的索引是 const(常量),t2 命中的索引是 sue 库的 t1 表的 id 字段。
rows 列
此列表示 MySQL 认为执行查询需要扫描的行数。

对于 InnoDB 引擎表,这个数字是一个估计值,可能并不总是准确的。
filtered 列
此列表示按条件过滤的行数所占表行数百分比的估算值。最大值为 100,这意味着没有过滤任何数据。从 100 开始数值减小表示增加数据过滤。

Rows 结果显示估算会扫描的行数,rows × filtered 结果表示与后面的表进行连接操作的行数。
例如,如果行数为 1,000,过滤为 50.00(50%),则与下表连接的行数为 1000 × 50% = 500。
extra 列
该字段包含有关 MySQL 如何解析查询的其他信息。这个列信息还是很重要的,但是里面的值太多了,就不一一介绍了,只列举几个常见的。
1. Impossible WHERE
假设指定 WHERE 后面的条件始终为 false。
1 | explain select code from test1 where 'a' = 'b'; |

2. Using filesort
表示按文件排序,一般出现在指定排序和索引排序不一致的情况下。
1 | explain select code from test1 order by name desc; |

这里创建了 code 和 name 的联合索引,顺序是 code 列在前,name 列在后;SQL 语句里按 name 字段直接降序,与之前的联合索引排序不同。
3. Using index
表示是否使用了覆盖索引,说白了就是获取的列值是否都经过了索引。

在上面的例子中,实际使用的是:Using index,因为只返回一列代码,所以对其字段进行了索引。
4. Using temporary
表示是否使用临时表,一般见于 order by 和 group by 语句。
1 | explain select name from test1 group by name; |

5. Using where
表示使用了 WHERE 条件过滤器。
6. Using join buffer
表示是否使用了连接缓冲,优先被连接的表的一部分数据被读入连接缓冲区,然后使用缓冲区中的数据与当前表执行连接操作。
下面是索引优化的过程:
- 首先,使用慢查询日志定位需要优化的
SQL语句; - 使用
explain查询计划查询索引使用情况; - 关注
key、key_len、type、extra信息,一般情况下,根据这四列就可以找到索引问题了; - 根据第
3步发现的索引问题优化SQL语句; - 返回到第
2步重复操作。
感谢您阅读本文,敬请期待更多精彩文章。