MySQL的数据是持久化的,意味着数据(索引+记录)是保存到磁盘上的,因为这样即使设备断电了,数据也不会丢失。
这里铺垫一些计算机基础知识:
- 磁盘读写的最小单位是扇区,扇区的大小只有512B大小
- 操作系统一次会读写多个扇区,所以操作系统的最小读写单位是块(Block)
- Linux 中的块大小为4KB,也就是一次磁盘 I/O 操作会直接读写8个扇区
由于数据库的索引是保存到磁盘上的,因此当我们通过索引查找某行数据的时候,就需要先从磁盘读取索引到内存,再通过索引从磁盘中找到某行数据,然后读入到内存,也就是说查询过程中会发生多次磁盘 I/O,而磁盘 I/O 次数越多,所消耗的时间也就越大。
另外,MySQL是支持范围查找的,所以索引的数据结构不仅要能高效地查询某一个记录,而且也要能高效地执行范围查找。
所以,要设计一个适合MySQL索引的数据结构,至少满足以下要求:
能在尽可能少的磁盘的 I/O 操作中完成查询工作
要能高效地查询某一个记录,也要能高效地执行范围查找
分析完要求后,我们针对每一个数据结构简要分析一下:
二分查找树虽然是一个天然的二分结构,能很好的利用二分查找快速定位数据,但是它存在一种极端的情况,每当插入的元素都是树内最大的元素,就会导致二分查找树退化成一个链表,此时查询复杂度就会从O(logn)降低为O(n)。
为了解决二分查找树退化成链表的问题,就出现了自平衡二叉树,保证了查询操作的时间复杂度就会一直维持在O(logn) 。但是它本质上还是一个二叉树,每个节点只能有2个子节点,随着元素的增多,树的高度会越来越高。而树的高度决定于磁盘 I/O 操作的次数,因为树是存储在磁盘中的,访问每个节点,都对应一次磁盘 I/O 操作,也就是说树的高度就等于每次查询数据时磁盘 IO 操作的次数,所以树的高度越高,就会影响查询性能。
B树和B+树都是通过多叉树的方式,会将树的高度变矮,所以这两个数据结构非常适合检索存于磁盘中的数据。但是MySQL默认的存储引擎InnoDB采用的是B+作为索引的数据结构,原因有:
说明:数据库的 I/O 操作的最小单位是页,InnoDB数据页的默认大小是16KB,意味着数据库每次读写都是以16KB为单位的,一次最少从磁盘中读取16K的内容到内存中,一次最少把内存中的16K内容刷新到磁盘中。
MySQL默认的存储引擎是InnoDB,它采用B+树作为索引的数据结构。在创建表时,InnoDB存储引擎默认会创建一个主键索引,也就是聚簇索引,其它索引都属于二级索引。
MySQL的MyISAM存储引擎支持多种索引数据结构,比如:B+树索引、R树索引、Full-Text索引。MyISAM存储引擎在创建表时,创建的主键索引默认使用的是B+树索引。
虽然,InnoDB和MyISAM都支持B+树索引,但是它们数据的存储结构实现方式不同:
InnoDB存储引擎:B+树索引的叶子节点保存数据本身
MyISAM存储引擎:B+树索引的叶子节点保存数据的物理地址
另外我们通过两个存储引擎的磁盘文件也可以发现有区别:
知识点扩展 >> 不同的存储引擎支持的索引类型如下图所示:
InnoDB存储引擎根据索引类型不同,分为聚簇索引和二级索引。它们区别在于:聚簇索引的叶子节点存放的是实际数据,所有完整的数据都存放在聚簇索引的叶子节点;而二级索引的叶子节点存放的是主键值,而不是实际数据,如下图所示:
在我们使用主键索引字段作为查询条件的时候,如果要查询的数据都在聚簇索引的叶子节点里,那么就会在聚簇索引中的B+树检索到对应的叶子节点,然后直接读取要查询的数据,如下面这条语句:
在我们使用二级索引字段作为查询条件的时候,如果要查询的数据在聚簇索引的叶子节点里,那么需要检索两颗B+树:
上面这个过程叫做回表,如下面这条语句:
在我们使用二级索引字段作为条件查询的时候,如果要查询的数据在二级索引的叶子节点,那么只需要在二级索引的B+树找到对应的叶子节点,然后读取要查询的数据,这个过程叫做覆盖索引。如下面这条语句:
注意,查询条件用上了索引列,并不意味着查询过程就一定都用上索引,接下来我们再一起看看哪些情况会导致【索引失效】,从而造成【全表扫描】。
这个问题只是个引子,其实是想盘一盘常见的6种会发生索引失效的场景:
当我们使用左或者左右模糊匹配的时候,也就是 like %xx
或者 like %xx%
这两种方式都会造成索引失效。为什么呢?
因为B+树索引是按照索引值有序排列存储的,只能根据前缀进行比较。
如果使用 name like '%林'
方式来查询,因为查询的结果可能是陈林、张林、周林等之类的,所以不知道从哪个索引值开始比较,于是就只能通过全表扫描的方式来查询。
有时候我们会用一些MySQL自带的函数来得到我们想要的结果,这时候需要注意了,如果查询条件中对索引字段使用函数,就会导致索引失效。为什么呢?
因为索引保存的是索引字段的原始值,而不是经过函数计算后的值,自然就没办法走索引了。
不过,从 MySQL 8.0 开始,索引特性增加了函数索引,即可以针对函数计算后的值建立一个索引,也就是说该索引的值是函数计算后的值,所以就可以通过扫描索引来查询数据。
举个栗子,通过下面这条语句,对 length(name) 的计算结果建立一个名为idx_name_length的索引:
alter table t_user add key idx_name_length ((length(name)));
然后再用下面这条查询语句,这时候就会走索引了,如下图所示:
当我们在查询条件中对索引列进行表达式计算,也是无法走索引的。看下面这个栗子:
原因跟对索引使用函数差不多。因为索引保存的是索引字段的原始值,而不是 id + 1 表达式计算后的值,所以无法走索引,只能通过把索引字段的取值都取出来,然后依次进行表达式的计算来进行条件判断,因此采用的就是全表扫描的方式。
有的同学可能会问,这种对索引进行简单的表达式计算,在代码特殊处理下,应该是可以做到索引扫描的,比方将 id + 1 = 10
变成 id = 10 - 1
。
是的,是能够实现,但是MySQL还是偷了这个懒,没有实现。如果非要较真的话,可以这么来理解:因为表达式计算的情况多种多样,每种都要考虑的话,代码可能会很臃肿,所以干脆将这种索引失效的场景告诉程序员,让程序员自己保证在查询条件中不要对索引进行表达式计算。
再多说一嘴,万一线上出现这种索引失效的情况,可以使用MySQL 5.7提供的 query_rewrite
功能,增加一个语句改写规则,如下所示:
insert into query_rewrite.rewrite_rules(
pattern
, replacement
, pattern_database
) values (
"select * from t where id + 1 = ?"
, "select * from t where id = ? - 1"
, "db1");
call query_rewrite.flush_rewrite_rules();
这里,call query_rewrite.flush_rewrite_rules()这个存储过程,是让插入的新规则生效,也就是我们说的查询重写,效果如下图所示:
学废了吧,就问你屌不屌吧!
对主键字段建立的索引叫做聚簇索引,对普通字段建立的索引叫做二级索引。
那么多个普通字段组合在一起创建的索引就叫做联合索引,也叫组合索引。
联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,否则就会导致索引失效。
假设创建了一个 (a, b, c) 联合索引,如果查询条件是以下这几种,就可以匹配上联合索引:
需要注意的是,因为有查询优化器,所以上面的字段a和字段b在where从句的顺序并不重要。
但是,如果查询条件是以下这几种,因为不符合最左匹配原则,所以就无法匹配上联合索引,联合索引就会失效:
为什么呢?原因是,在联合索引的情况下,数据是按照索引第一列排序,第一列数据相同时才会按照第二列排序。也就是说,如果我们想使用联合索引中尽可能多的列,查询条件中的各个列必须是联合索引中从最左边开始连续的列。如果我们仅仅按照第二列搜索,肯定无法走索引。
有一个比较特殊的查询条件:where a = 1 and c = 3
,符合最左匹配吗?
这种其实严格意义上来说是属于索引截断,不同版本处理方式也不一样:
MySQL 5.5 的话,前面字段a会走索引,在联合索引找到主键值后,开始回表,到主键索引读取数据行,然后再比对字段c的值。
从 MySQL 5.6 之后,有一个索引下推功能(参考 Index Condition Pushdown 优化),可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。
关于索引截断,再啰嗦一句,除了刚才那种情况,范围查询也会造成索引截断,这一点我在 你可能不够了解的NULL和IN 这篇博客中提到了,当然也可以参考MySQL官方文档 Range Optimization,如下图所示:
在WHERE从句中,如果在OR前的条件列是索引列,而在OR后的条件列不是索引列,那么索引会失效。
举个栗子,比如下面的查询语句,id 是主键,age 是普通列,从执行计划的结果看,是走了全表扫描:
这是因为OR的含义就是两个只要满足一个即可,因此只有一个条件列是索引列是没有意义的,只要有条件列不是索引列,就会进行全表扫描。
要解决这个问题也很简单,将age字段设置为索引即可,如下图所示:
可以看到 type=index_merge
, index_merge
的意思就是对id和age分别进行了扫描,然后将这两个结果集进行了合并,这样做的好处就是避免了全表扫描。
如果索引字段是字符串类型,但是在条件查询中,输入的参数是整型的话,会在执行计划中发现这条语句走的是全表扫描。看下面这个栗子,字段phone是varchar类型:
为什么会这样?这么简单的类型转换,如果MySQL不直接处理了,就不必做全表扫描了。。。甚至有同学可能会认为MySQL不做隐式类型转换比刚才索引表达式的问题还要脑残,就问你们是不是这么想的吧。
要讲明白这个原因,我们首先需要搞清楚的是MySQL做数据类型转换规则是什么,说白了,就是要搞清楚MySQL是会将字符串转成数字处理,还是将数字转换成字符串处理。
我们先来看一个简单的栗子:
上面的返回结果为1,说明MySQL在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较。
再举一个栗子来证明这个结论,还是刚才那个SQL,这次我们把where条件改成int类型的主键id:
虽然id是整型,虽然查询条件是字符串 '1'
,但是这条语句还是走了索引扫描。
我相信很多同学都看过或者听人说过如果有隐式转换的话,MySQL不走索引,而我想告诉大家的是,不要随波逐流,不要人云亦云,这样真的太LOW了,要学会自己去判断!!!
我们再来看看那个因为隐式转换走全表扫描的SQL:
因为phone字段为字符串,所以MySQL会自动把字符串转为数字,所以这条语句等价于:
现在清楚了吧,CAST函数是作用在了phone字段上,而phone字段是索引字段,也就是对索引使用了函数!前面我们已经说过了,对索引使用函数是会导致索引失效的。
最后分析下那条有隐式转换但依然走了索引的SQL:
可以看到这时字符串部分是输入参数,需要将字符串转为数字,所以这条语句等价于:
这一次索引字段并没有用任何函数,CAST函数是用在了输入参数上,因此是可以走索引扫描的。
其实还有一种更加难以察觉的隐式转换陷阱,如下图所示:
上图中3张表的name字段都是varchar类型,但是由于编码不同也会使得索引失效。因为字符集utf8mb4是utf8的超集,MySQL内部的实现为了避免数据在转换过程中造成数据截断,会按数据长度增加的方向进行转换,即把utf8字符串转成utf8mb4字符串,所以依然是在索引字段上使用函数导致的索引失效。
就问你有没有豁然开朗的感觉!?不要再幼稚地以为MySQL是不是脑残了,居然对隐式转换见死不救。我只想告诉你,你可能真的不够了解MySQL,所以接着往下看吧,后面还有干货哦!
如果你对【Sending data】很陌生的话,请先看看下图:
相信很多同学对【Sending data】的理解是【正在发送数据】,其实是被误导了。我们先来看看大多数同学理解的【正在发送数据】究竟对应哪个状态。
假设MySQL服务器内存是64G,在test库有一张表t,其占用的磁盘空间是100G,如果执行下面的操作:
mysql -h$host -P$port -u$user -p$pwd \
-e "select * from db1.t" > $target_file
显然,上面会执行【全表扫描】,请问服务器的内存会被用光吗?答案是否定的。为什么呢?
前面已经提到,InnoDB数据保存在主键索引上,所以全表扫描实际上是直接扫描表t的主键索引。这条查询语句由于没有其他判断条件,所以查到的每一行都可以直接放到结果集,然后返回给客户端。
那么,这个【结果集】存在哪里呢?
其实服务端无需在内存中保存完整结果集,取数据和发数据的流程如下:
获取一行,写到 net_buffer
。这块内存的大小是由参数 net_buffer_length
定义,默认16k
重复获取行,直到 net_buffer
写满,调用网络接口发出去
若发送成功,就清空 net_buffer
,然后继续取下一行,并写入 net_buffer
若发送函数返回 EAGAIN
或 WSAEWOULDBLOCK
,就表示本地网络栈(socket send buffer)写满了,进入等待。直到网络栈重新可写,再继续发送
从上述流程中,可以看到:
一个查询在发送过程中,占用的MySQL内部的内存最大就是 net_buffer_length
这么大,不会达到100G
socket send buffer 也不可能达到100G(默认定义在/proc/sys/net/core/wmem_default),若socket send buffer被写满,就会暂停读数据的流程
MySQL其实是边读边发,这意味着,若客户端接收得慢,会导致MySQL服务端由于结果发不出去,造成这个事务的执行时间变长
那么当客户端不读socket receive buffer内容时,服务端是什么状态呢?请看下图:
讲到这里,大家有没有发现【Sending to client】才是我们理解的【正在发送数据】。
说明:在MySQL 5.7.8之前
Sending to client
被称为Writing to net
。
补充一个小知识点,MySQL客户端有一个 --quick
参数,如下图所示:
加上这个参数后,会使用 mysql_use_result
方法,即读一行处理一行。假设某个业务逻辑很复杂,每读一行数据后要处理很长时间,就会导致客户端要过很久才取下一行数据,可能就会出现上面【Sending to client】的情况。
因此,对于正常的线上业务来说,若一个查询的返回结果不多,推荐使用 mysql_store_result
接口,即直接把查询结果保存到本地内存。
如果MySQL里看到很多个线程都处于【Sending to client】,这时可能就需要让业务开发同学优化查询结果,并评估这么多的返回结果是否合理。若要快速减少处于这个状态的线程数,可以将 net_buffer_length
设置得更大。
相比【Sending to client】这个状态,日常我们更多看到的是【Sending data】,但查看网络也没什么问题,为什么Sending data要这么久?先来看看MySQL官网对【Sending data】的解释:
其实,一个查询语句的状态变化是这样的:
也就是说,【Sending data】并不一定是指【正在发送数据】,而可能是处于执行器过程中的任意阶段,所以我们经常看到的是【Sending data】而不是【Sending to client】。
但凡提到MySQL查询优化,大多数同学应该都能说出 EXPLAIN
。不过,我也相信大部分同学仅仅知道有EXPLAIN,至于是否还有其他的就只能呵呵了。。。
在讲三板斧之前,我们先做个简单的总结。
在MySQL中,会引发性能问题的慢查询,大体有以下三种可能:
一般地,碰到慢SQL,经过分析后,如果判断需要人为干预,那么常见的优化手段有:
清楚了上述原因和手段后,就可以对症下药,这就是所谓的【理论指导实践】,不至于乱打一通。
通常,我们在执行一条SQL时,想要知道这个SQL先后查询了哪些表,是否使用了索引,这些数据从哪里获取到,获取到数据遍历了多少行数据等等,我们可以通过EXPLAIN命令来查看这些执行信息,如下图所示:
注意:如果执行完EXPLAIN后有 warning
,一定记得 show warnings
,可能会发现有用的信息,比如上面截图里执行 show warnings
后会发现原SQL被改写了。
下面对图示中的每一个字段进行一个说明,从中你也能收获到很多零散的知识点。
<derivedN>
格式,表示当前查询依赖 id=N 的查询,于是先执行 id=N 的查询。UNION RESULT
的table列的值为 <unionM,N>
,M和N表示参与UNION的SELECT行id。<subqueryN>
,更多信息可参考 Materialization优化。force index
、ignore index
。create index idx_blob on t(content(150));
innodb_large_prefix
被启用,对于使用 DYNAMIC
或 COMPRESSED
行格式的InnoDB表,索引键前缀的限制是3072字节innodb_large_prefix
被禁用,对于任何行格式的表,索引键的前缀限制是767字节func
SHOW WARNINGS
查看扩展的EXPLAIN输出Table Filter
(在分析WHERE条件时涉及到 Index Key、Table Filter 和 Index Filter,参考 复杂 SQL 加锁分析)来对读取出来的行数据进行过滤,理论上,读取出来的行等于返回结果的行数时效率最高,过滤的比率越多,效率越低。Using where; Using index
:SELECT的列被索引覆盖,并且WHERE筛选条件是索引列之一,但不是索引的前导列,意味着无法直接通过索引查找来查询到符合条件的数据。更多EXPLAIN细节详见MySQL官方文档 EXPLAIN Output Format 。
上述通过EXPLAIN分析执行计划,仅仅是停留在分析SQL的外部的执行情况,如果我们想要深入到MySQL内核中,从执行线程的状态和时间来分析的话,这个时候我们就可以选择PROFILING。
PROFILING除了可以分析执行线程的状态和时间,还支持进一步选择 ALL、CPU、MEMORY、BLOCK IO、CONTEXT SWITCHES 等类型来查看SQL语句在不同系统资源上所消耗的时间。以下是相关命令的注释:
在使用PROFILING功能前,我们先查看PROFILING信息:
PROFILING一般操作如下图所示:
说明:
SET profiling=1
开启SESSION级别的PROFILING,只记录当前SESSION的PROFILING。
profiling_history_size
默认值为15,最大值为100,如果将该值设置为0的实际效果是禁用PROFILING。
提交SQL后,无论是否有语法错误、无论是否中途被kill,都会被记录到PROFILING的历史记录中。
show profile 之类的语句不会被PROFILING,即自身不会产生PROFILING。
大部分情况下,在PROFILING的结果中我们主要关注两列:Status
和 Duration
。Status表示的是PROFILING里的状态,它和PROCESSLIST的 State
基本是一致的;Duration表示该状态的耗时。
我们主要关注处于哪个状态耗时最久(比如前面提到的 Sending data
),这些状态中,哪些可以进一步优化。
我们还可以通过观察某些阶段是否存在,以及如何避免这些阶段的出现进行优化,比如避免内存排序的出现。
PROFILING功能即将废弃(从MySQL 5.6.7开始标记为 deprecated),改用 Performance Schema
代替,参考 Query Profiling Using Performance Schema,如下图所示:
利用EXPLAIN能展示SQL语句的执行计划,却无法展示为什么一些其他的执行计划未被选择,比如说明明有索引,但是为什么查询时未使用索引等。为此,MySQL 5.6 及之后的版本中提供了 Optimizer Trace
功能,让我们能更加详细的了解SQL语句执行的所有替代计划、估计成本、优化和选择过程。
在讲 Optimizer Trace 的之前,我们先来说说查询成本这个概念。
MySQL优化器首先会计算出全表扫描的成本,然后选出该SQL可能涉及到的所有索引并且计算索引的成本,然后选出所有成本最小的那个来执行,关于MySQL成本优化器可以参考 Cost-based Optimizer。这里说的成本是指基于数据采样计算的预估成本,可能无法正确反映整体的情况,这也是MySQL优化器有时做出错误优化的重要原因之一。
OPTIMIZER TRACE 相关参数如下:
OPTIMIZER TRACE 使用步骤如下:
-- 打开 optimizer_trace
SET optimizer_trace="enabled=on";
-- 设置 optimizer_trace 最大内存占用
SET optimizer_trace_max_mem_size=1000000;
-- 执行我们想要分析的SQL
SELECT * FROM order_info where uid = 5837661 order by id asc limit 1;
-- 查看执行计划表
SELECT * FROM information_schema.OPTIMIZER_TRACE;
-- 关闭 optimizer_trace
SET optimizer_trace="enabled=off";
查询计划表 OPTIMIZER_TRACE
有4个列,如下所示:
其中,最为重要的就是第二列TRACE,其内容是一个超级大的JSON,主要结构如下:
可以看到,它有三大块内容,也代表着SQL语句处理的三个阶段,分别为准备阶段,优化阶段和执行阶段:
rows_estimation
这个过程,这个过程深入分析了对单表查询的各种执行方案的成本considered_execution_plans
这个过程,这个过程里会写明各种不同的连接方式所对应的成本。优化器会选择成本最低的那种方案来作为最终的执行计划,也就是我们使用EXPLAIN语句所展现出的那种方案。接下来我们以一个栗子来说明 OPTIMIZER TRACE 的使用:
可以看到该查询可能使用到的索引有3个,那么为什么优化器最终选择了idx_key2而不选择其他的索引或者直接全表扫描呢?这时候就可以通过otpimzer trace功能来查看优化器的具体工作过程。
我们直接看一下通过查询OPTIMIZER_TRACE表得到的输出(#后跟随的注释解释了优化过程中的一些比较重要的点,大家重点关注一下):
*************************** 1. row ***************************
# 分析的查询语句是什么
QUERY: SELECT * FROM s1 WHERE
key1 > 'z' AND
key2 < 1000000 AND
key3 IN ('a', 'b', 'c') AND
common_field = 'abc'
# 优化的具体过程
TRACE: {
"steps": [
{
"join_preparation": { # prepare阶段
"select#": 1,
"steps": [
{
"IN_uses_bisection": true
},
{
"expanded_query": "/* select#1 */ select `s1`.`id` AS `id`,`s1`.`key1` AS `key1`,`s1`.`key2` AS `key2`,`s1`.`key3` AS `key3`,`s1`.`key_part1` AS `key_part1`,`s1`.`key_part2` AS `key_part2`,`s1`.`key_part3` AS `key_part3`,`s1`.`common_field` AS `common_field` from `s1` where ((`s1`.`key1` > 'z') and (`s1`.`key2` < 1000000) and (`s1`.`key3` in ('a','b','c')) and (`s1`.`common_field` = 'abc'))"
}
] /* steps */
} /* join_preparation */
},
{
"join_optimization": { # optimize阶段
"select#": 1,
"steps": [
{
"condition_processing": { # 处理搜索条件
"condition": "WHERE",
# 原始搜索条件
"original_condition": "((`s1`.`key1` > 'z') and (`s1`.`key2` < 1000000) and (`s1`.`key3` in ('a','b','c')) and (`s1`.`common_field` = 'abc'))",
"steps": [
{
# 等值传递转换
"transformation": "equality_propagation",
"resulting_condition": "((`s1`.`key1` > 'z') and (`s1`.`key2` < 1000000) and (`s1`.`key3` in ('a','b','c')) and (`s1`.`common_field` = 'abc'))"
},
{
# 常量传递转换
"transformation": "constant_propagation",
"resulting_condition": "((`s1`.`key1` > 'z') and (`s1`.`key2` < 1000000) and (`s1`.`key3` in ('a','b','c')) and (`s1`.`common_field` = 'abc'))"
},
{
# 去除没用的条件
"transformation": "trivial_condition_removal",
"resulting_condition": "((`s1`.`key1` > 'z') and (`s1`.`key2` < 1000000) and (`s1`.`key3` in ('a','b','c')) and (`s1`.`common_field` = 'abc'))"
}
] /* steps */
} /* condition_processing */
},
{
# 替换虚拟生成列
"substitute_generated_columns": {
} /* substitute_generated_columns */
},
{
# 表的依赖信息
"table_dependencies": [
{
"table": "`s1`",
"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": "`s1`",
"range_analysis": {
"table_scan": { # 全表扫描的行数以及成本
"rows": 9688,
"cost": 2036.7
} /* table_scan */,
# 分析可能使用的索引
"potential_range_indexes": [
{
"index": "PRIMARY", # 主键不可用
"usable": false,
"cause": "not_applicable"
},
{
"index": "idx_key2", # idx_key2可能被使用
"usable": true,
"key_parts": [
"key2"
] /* key_parts */
},
{
"index": "idx_key1", # idx_key1可能被使用
"usable": true,
"key_parts": [
"key1",
"id"
] /* key_parts */
},
{
"index": "idx_key3", # idx_key3可能被使用
"usable": true,
"key_parts": [
"key3",
"id"
] /* key_parts */
},
{
"index": "idx_key_part", # idx_keypart不可用
"usable": false,
"cause": "not_applicable"
}
] /* potential_range_indexes */,
"setup_range_conditions": [
] /* setup_range_conditions */,
"group_index_range": {
"chosen": false,
"cause": "not_group_by_or_distinct"
} /* group_index_range */,
# 分析各种可能使用的索引的成本
"analyzing_range_alternatives": {
"range_scan_alternatives": [
{
# 使用idx_key2的成本分析
"index": "idx_key2",
# 使用idx_key2的范围区间
"ranges": [
"NULL < key2 < 1000000"
] /* ranges */,
"index_dives_for_eq_ranges": true, # 是否使用index dive
"rowid_ordered": false, # 使用该索引获取的记录是否按照主键排序
"using_mrr": false, # 是否使用mrr
"index_only": false, # 是否是索引覆盖访问
"rows": 12, # 使用该索引获取的记录条数
"cost": 15.41, # 使用该索引的成本
"chosen": true # 是否选择该索引
},
{
# 使用idx_key1的成本分析
"index": "idx_key1",
# 使用idx_key1的范围区间
"ranges": [
"z < key1"
] /* ranges */,
"index_dives_for_eq_ranges": true, # 同上
"rowid_ordered": false, # 同上
"using_mrr": false, # 同上
"index_only": false, # 同上
"rows": 266, # 同上
"cost": 320.21, # 同上
"chosen": false, # 同上
"cause": "cost" # 因为成本太大所以不选择该索引
},
{
# 使用idx_key3的成本分析
"index": "idx_key3",
# 使用idx_key3的范围区间
"ranges": [
"a <= key3 <= a",
"b <= key3 <= b",
"c <= key3 <= c"
] /* ranges */,
"index_dives_for_eq_ranges": true, # 同上
"rowid_ordered": false, # 同上
"using_mrr": false, # 同上
"index_only": false, # 同上
"rows": 21, # 同上
"cost": 28.21, # 同上
"chosen": false, # 同上
"cause": "cost" # 同上
}
] /* range_scan_alternatives */,
# 分析使用索引合并的成本
"analyzing_roworder_intersect": {
"usable": false,
"cause": "too_few_roworder_scans"
} /* analyzing_roworder_intersect */
} /* analyzing_range_alternatives */,
# 对于上述单表查询s1最优的访问方法
"chosen_range_access_summary": {
"range_access_plan": {
"type": "range_scan",
"index": "idx_key2",
"rows": 12,
"ranges": [
"NULL < key2 < 1000000"
] /* ranges */
} /* range_access_plan */,
"rows_for_plan": 12,
"cost_for_plan": 15.41,
"chosen": true
} /* chosen_range_access_summary */
} /* range_analysis */
}
] /* rows_estimation */
},
{
# 分析各种可能的执行计划
#(对多表查询这可能有很多种不同的方案,单表查询的方案上边已经分析过了,直接选取idx_key2就好)
"considered_execution_plans": [
{
"plan_prefix": [
] /* plan_prefix */,
"table": "`s1`",
"best_access_path": {
"considered_access_paths": [
{
"rows_to_scan": 12,
"access_type": "range",
"range_details": {
"used_index": "idx_key2"
} /* range_details */,
"resulting_rows": 12,
"cost": 17.81,
"chosen": true
}
] /* considered_access_paths */
} /* best_access_path */,
"condition_filtering_pct": 100,
"rows_for_plan": 12,
"cost_for_plan": 17.81,
"chosen": true
}
] /* considered_execution_plans */
},
{
# 尝试给查询添加一些其他的查询条件
"attaching_conditions_to_tables": {
"original_condition": "((`s1`.`key1` > 'z') and (`s1`.`key2` < 1000000) and (`s1`.`key3` in ('a','b','c')) and (`s1`.`common_field` = 'abc'))",
"attached_conditions_computation": [
] /* attached_conditions_computation */,
"attached_conditions_summary": [
{
"table": "`s1`",
"attached": "((`s1`.`key1` > 'z') and (`s1`.`key2` < 1000000) and (`s1`.`key3` in ('a','b','c')) and (`s1`.`common_field` = 'abc'))"
}
] /* attached_conditions_summary */
} /* attaching_conditions_to_tables */
},
{
# 再稍稍的改进一下执行计划
"refine_plan": [
{
"table": "`s1`",
"pushed_index_condition": "(`s1`.`key2` < 1000000)",
"table_condition_attached": "((`s1`.`key1` > 'z') and (`s1`.`key3` in ('a','b','c')) and (`s1`.`common_field` = 'abc'))"
}
] /* refine_plan */
}
] /* steps */
} /* join_optimization */
},
{
"join_execution": { # execute阶段
"select#": 1,
"steps": [
] /* steps */
} /* join_execution */
}
] /* steps */
}
# 因优化过程文本太多而丢弃的文本字节大小,值为0时表示并没有丢弃
MISSING_BYTES_BEYOND_MAX_MEM_SIZE: 0
# 权限字段
INSUFFICIENT_PRIVILEGES: 0
1 row in set (0.00 sec)