目录
SQL_SMALL_RESULT和SQL_BIG_RESULT
USE INDEX、IGNORE INDEX和FORCE INDEX
一条sql语句是如何执行的
当希望MySQL能够以更高的性能运行查询时,最好的办法就是弄清楚MySQL是如何优化和执行查询的。一且理解这一点,很多查询优化工作实际上就是遵循一些原则让优化器能够按照预想的合理的方式运行。
MySQL 基本架构概览
下图是 MySQL 的一个简要架构图,从下图你可以很清晰的看到用户的 SQL 语句在 MySQL 内部是如何执行的。
先简单介绍一下下图涉及的一些组件的基本作用帮助大家理解这幅图,在 1.2 节中会详细介绍到这些组件的作用。
连接器: 身份认证和权限相关(登录 MySQL 的时候)。
查询缓存: 执行查询语句的时候,会先查询缓存(MySQL 8.0 版本后移除,因为这个功能不太实用)。
分析器: 没有命中缓存的话,SQL 语句就会经过分析器,分析器说白了就是要先看你的 SQL 语句要干嘛,再检查你的 SQL 语句语法是否正确。
优化器: 按照 MySQL 认为最优的方案去执行。
执行器: 执行语句,然后从存储引擎返回数据。
简单来说 MySQL 主要分为 Server 层和存储引擎层:
Server 层:主要包括连接器、查询缓存、分析器、优化器、执行器等,所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图,函数等,还有一个通用的日志模块 binglog 日志模块。
存储引擎: 主要负责数据的存储和读取,采用可以替换的插件式架构,支持 InnoDB、MyISAM、Memory 等多个存储引擎,其中 InnoDB 引擎有自有的日志模块 redolog 模块。现在最常用的存储引擎是 InnoDB,它从 MySQL 5.5.5 版本开始就被当做默认存储引擎了。
Server 层基本组件介绍
1) 连接器
连接器主要和身份认证和权限相关的功能相关,就好比一个级别很高的门卫一样。
主要负责用户登录数据库,进行用户的身份认证,包括校验账户密码,权限等操作,如果用户账户密码已通过,连接器会到权限表中查询该用户的所有权限,之后在这个连接里的权限逻辑判断都是会依赖此时读取到的权限数据,也就是说,后续只要这个连接不断开,即时管理员修改了该用户的权限,该用户也是不受影响的。
2) 查询缓存(MySQL 8.0 版本后移除)
查询缓存主要用来缓存我们所执行的 SELECT 语句以及该语句的结果集。
连接建立后,执行查询语句的时候,会先查询缓存,MySQL 会先校验这个 sql 是否执行过,以 Key-Value 的形式缓存在内存中,Key 是查询预计,Value 是结果集。如果缓存 key 被命中,就会直接返回给客户端,如果没有命中,就会执行后续的操作,完成后也会把结果缓存起来,方便下一次调用。当然在真正执行缓存查询的时候还是会校验用户的权限,是否有该表的查询条件。
MySQL 查询不建议使用缓存,因为查询缓存失效在实际业务场景中可能会非常频繁,假如你对一个表更新的话,这个表上的所有的查询缓存都会被清空。对于不经常更新的数据来说,使用缓存还是可以的。
所以,一般在大多数情况下我们都是不推荐去使用查询缓存的。
MySQL 8.0 版本后删除了缓存的功能,官方也是认为该功能在实际的应用场景比较少,所以干脆直接删掉了。
3) 分析器和预处理
MySQL 没有命中缓存,那么就会进入分析器,分析器主要是用来分析 SQL 语句是来干嘛的,分析器也会分为几步:
mysql通过关键字将SQL语句进行解析,并生成一颗解析树,mysql解析器将使用mysql语法规则验证和解析查询,例如验证使用使用了错误的关键字或者顺序是否正确等等,预处理器会进一步检查解析树是否合法,例如表名和列名是否存在,是否有歧义,还会验证权限等等
第一步,词法分析,一条 SQL 语句有多个字符串组成,首先要提取关键字,比如 select,提出查询的表,提出字段名,提出查询条件等等。做完这些操作后,就会进入第二步。
第二步,语法分析,主要就是判断你输入的 sql 是否正确,是否符合 MySQL 的语法。
完成这 2 步之后,MySQL 就准备开始执行了,但是如何执行,怎么执行是最好的结果呢?这个时候就需要优化器上场了。
4) 优化器
当语法树没有问题之后,相应的要由优化器将其转成执行计划,一条查询语句可以使用非常多的执行方式,最后都可以得到对应的结果,但是不同的执行方式带来的效率是不同的,优化器的最主要目的就是要选择最有效的执行计划(有时候可能也不是最优,这篇文章涉及对这部分知识的深入讲解),比如多个索引的时候该如何选择索引,多表查询的时候如何选择关联顺序等。
mysql使用的是基于成本的优化器,在优化的时候会尝试预测一个查询使用某种查询计划时候的成本,并选择其中成本最小的一个
可以说,经过了优化器之后可以说这个语句具体该如何执行就已经定下来。
5) 执行器
当选择了执行方案后,MySQL 就准备开始执行了,首先执行前会校验该用户有没有权限,如果没有权限,就会返回错误信息,如果有权限,就会去调用引擎的接口,返回接口执行的结果。
语句分析
查询语句
说了以上这么多,那么究竟一条 sql 语句是如何执行的呢?其实我们的 sql 可以分为两种,一种是查询,一种是更新(增加,更新,删除)。我们先分析下查询语句,语句如下:
select * from tb_student A where A.age='18' and A.name=' 张三 ';
结合上面的说明,我们分析下这个语句的执行流程:
先检查该语句是否有权限,如果没有权限,直接返回错误信息,如果有权限,在 MySQL8.0 版本以前,会先查询缓存,以这条 sql 语句为 key 在内存中查询是否有结果,如果有直接缓存,如果没有,执行下一步。
通过分析器进行词法分析,提取 sql 语句的关键元素,比如提取上面这个语句是查询 select,提取需要查询的表名为 tb_student,需要查询所有的列,查询条件是这个表的 id=‘1’。然后判断这个 sql 语句是否有语法错误,比如关键词是否正确等等,如果检查没问题就执行下一步。
接下来就是优化器进行确定执行方案,上面的 sql 语句,可以有两种执行方案:
a. 先查询学生表中姓名为“张三”的学生,然后判断是否年龄是 18。
b. 先找出学生中年龄 18 岁的学生,然后再查询姓名为“张三”的学生。
那么优化器根据自己的优化算法进行选择执行效率最好的一个方案(优化器认为,有时候不一定最好)。那么确认了执行计划后就准备开始执行了。
进行权限校验,如果没有权限就会返回错误信息,如果有权限就会调用数据库引擎接口,返回引擎的执行结果。
更新语句
以上就是一条查询 sql 的执行流程,那么接下来我们看看一条更新语句如何执行的呢?sql 语句如下:
update tb_student A set A.age='19' where A.name=' 张三 ';
我们来给张三修改下年龄,在实际数据库肯定不会设置年龄这个字段的,不然要被技术负责人打的。其实条语句也基本上会沿着上一个查询的流程走,只不过执行更新的时候肯定要记录日志啦,这就会引入日志模块了,MySQL 自带的日志模块式 binlog(归档日志) ,所有的存储引擎都可以使用,我们常用的 InnoDB 引擎还自带了一个日志模块 redo log(重做日志),我们就以 InnoDB 模式下来探讨这个语句的执行流程。流程如下:
先查询到张三这一条数据,如果有缓存,也是会用到缓存。
然后拿到查询的语句,把 age 改为 19,然后调用引擎 API 接口,写入这一行数据,InnoDB 引擎把数据保存在内存中,同时记录 redo log,此时 redo log 进入 prepare 状态,然后告诉执行器,执行完成了,随时可以提交。
执行器收到通知后记录 binlog,然后调用引擎接口,提交 redo log 为提交状态。
更新完成。
这里肯定有同学会问,为什么要用两个日志模块,用一个日志模块不行吗?
这是因为最开始 MySQL 并没与 InnoDB 引擎( InnoDB 引擎是其他公司以插件形式插入 MySQL 的) ,MySQL 自带的引擎是 MyISAM,但是我们知道 redo log 是 InnoDB 引擎特有的,其他存储引擎都没有,这就导致会没有 crash-safe 的能力(crash-safe 的能力即使数据库发生异常重启,之前提交的记录都不会丢失),binlog 日志只能用来归档。
并不是说只用一个日志模块不可以,只是 InnoDB 引擎就是通过 redo log 来支持事务的。那么,又会有同学问,我用两个日志模块,但是不要这么复杂行不行,为什么 redo log 要引入 prepare 预提交状态?这里我们用反证法来说明下为什么要这么做?
先写 redo log 直接提交,然后写 binlog,假设写完 redo log 后,机器挂了,binlog 日志没有被写入,那么机器重启后,这台机器会通过 redo log 恢复数据,但是这个时候 bingog 并没有记录该数据,后续进行机器备份的时候,就会丢失这一条数据,同时主从同步也会丢失这一条数据。
先写 binlog,然后写 redo log,假设写完了 binlog,机器异常重启了,由于没有 redo log,本机是无法恢复这一条记录的,但是 binlog 又有记录,那么和上面同样的道理,就会产生数据不一致的情况。
如果采用 redo log 两阶段提交的方式就不一样了,写完 binglog 后,然后再提交 redo log 就会防止出现上述的问题,从而保证了数据的一致性。那么问题来了,有没有一个极端的情况呢?假设 redo log 处于预提交状态,binglog 也已经写完了,这个时候发生了异常重启会怎么样呢? 这个就要依赖于 MySQL 的处理机制了,MySQL 的处理过程如下:
判断 redo log 是否完整,如果判断是完整的,就立即提交。
如果 redo log 只是预提交但不是 commit 状态,这个时候就会去判断 binlog 是否完整,如果完整就提交 redo log, 不完整就回滚事务。
这样就解决了数据一致性的问题。
执行流程总结
MySQL 主要分为 Server 层和引擎层,Server 层主要包括连接器、查询缓存、分析器、优化器、执行器,同时还有一个日志模块(binlog),这个日志模块所有执行引擎都可以共用,redolog 只有 InnoDB 有。
引擎层是插件式的,目前主要包括,MyISAM,InnoDB,Memory 等。
SQL 等执行过程分为两类,一类对于查询等过程如下:权限校验—》查询缓存—》分析器—》优化器—》权限校验—》执行器—》引擎
对于更新等语句执行流程如下:分析器----》权限校验----》执行器—》引擎—redo log prepare—》binlog—》redo log commit
mysql的页
首先Mysql的基本存储结构是页(记录都存在页里边):
各个数据页可以组成一个双向链表
而每个数据页中的记录又可以组成一个单向链表
每个数据页都会为存储在它里边儿的记录生成一个页目录,在通过主键查找某条记录的时候可以在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定的记录
以其他列(非主键)作为搜索条件:只能从最小记录开始依次遍历单链表中的每条记录。
所以说,如果我们写 select * from user where username='丙丙'这样没有进行任何优化的sql语句,默认会这样做:
定位到记录所在的页
需要遍历双向链表,找到所在的页
从所在的页内中查找相应的记录
由于不是根据主键查询,只能遍历所在页的单链表了
很明显,在数据量很大的情况下这样查找会很慢!看起来跟回表有点点像。
MySQL客户端/服务器通信协议
一般来说,不需要去理解MySQL通信协议的内部实现细节,只需要大致理解通信协议 是如何工作的。MySQL客户端和服务器之间的通信协议是“半双工”的,这意味着, 在任何一个时刻,要么是由服务器向客户端发送数据,要么是由客户端向服务器发送 数据,这两个动作不能同时发生。所以,我们无法也无须将一个消息切成小块独立来 发送。
这种协议让MySQL通信简单快速,但是也从很多地方限制了MySQL。一个明显的限制 是,这意味着没法进行流量控制。一且一端开始发生消息,另一端要接收完整个消息才 能响应它。这就像来回抛球的游戏:在任何时刻,只有一个人能控制球,而且只有控制 球的人才能将球抛回去(发送消息)。
客户端用一个单独的数据包将查询传给服务器。这也是为什么当查询的语句很长的时候, 参数max_ al lowed _packet就特别重要了。一旦客户端发送了请求,它能做的事情就只 是等待结果了。
相反的,一般服务器响应给用户的数据通常很多,由多个数据包组成。当服务器开始响应客户端请求时,客户端必须完整地接收整个返回结果,而不能简单地只取前面几条结 果,然后让服务器停止发送数据。这种情况下,客户端若接收完整的结果,然后取前面 几条需要的结果,或者接收完几条结果后就“粗暴“地断开连接,都不是好主意。这也 是在必要的时候一定要在查询中加上LIMIT限制的原因。
换一种方式解释这种行为:当客户端从服务器取数据时,看起来是一个拉数据的过程, 但实际上是MySQL在向客户端推送数据的过程。客户端不断地接收从服务器推送的数 据,客户端也没法让服务器停下来。客户端像是“从消防水管喝水”(这是一个术语)。
多数连接MySQL的库函数都可以获得全部结果集并缓存到内存里,还可以逐行获取需要的数据。默认一般是获得全部结果集并缓存到内存中。MySQL通常需要等所有的数据都已经发送给客户端才能释放这条查询所占用的资源,所以接收全部结果并缓存通常 可以减少服务器的压力,让查询能够早点结束、早点释放相应的资源。
当使用多数连接MySQL的库函数从MySQL获取数据时,其结果看起来都像是从 MySQL服务器获取数据,而实际上都是从这个库函数的缓存获取数据。(相当于调用mysql的那一方,比如java,php等等,内部建立了一个临时空间,作为应用和mysql的中转站)。
多数情况下这 没什么问题,但是如果需要返回一个很大的结果集的时候,这样做井不好,因为库函数 会花很多时间和内存来存储所有的结果集。如果能够尽早开始处理这些结果集,就能大 大减少内存的消耗,这种情况下可以不使用缓存来记录结果而是直接处理。(不同的语言有不同的方式,来不缓存查询结果,直接处理结果)这样做的缺 点是,对于服务器来说,需要查询完成后才能释放资源,所以在和客户端交互的整个过 程中,服务器的资源都是被这个查询所占用的。
查询状态
对于一个MySQL连接,或者说一个线程,任何时刻都有一个状态,该状态表示了 MySQL当前正在做什么。有很多种方式能查看当前的状态,最简单的是使用SHOW FULL PROCESSLIST命令(该命令返回结果中的Command列就表示当前的状态)。在一个查询的 生命周期中,状态会变化很多次。MySQL官方手册中对这些状态值的含义有最权威的 解释,下面将这些状态列出来,井做一个简单的解释。
Sleep
线程正在等待客户端发送新的请求。
Query
线程正在执行查询或者正在将结果发送给客户端。
Locked
在MySQL服务器层,该线程正在等待表锁。在存储引擎级别实现的锁,例如 InnoDB的行锁,并不会体现在线程状态中。对于MylSAM来说这是一个比较典型 的状态,但在其他没有行锁的引擎中也经常会出现。
Analyzing and statistics
线程正在收集存储引擎的统计信息,并生成查询的执行计划。
Copying to tmp table [on disk]
线程正在执行查询,并且将其结果集都复制到一个临时表中,这种状态一般要么是 在做GROUP BY操作,要么是文件排序操作,或者是UNION操作。如果这个状态后面 还有"on disk"标记,那表示MySQL正在将一个内存临时表放到磁盘上。
Sorting result
线程正在对结果集进行排序。
Sending data
这表示多种情况:线程可能在多个状态之间传送数据,或者在生成结果集,或者在 向客户端返回数据。
了解这些状态的基本含义非常有用,这可以让你很快地了解当前“谁正在持球”。在一 个繁忙的服务器上,可能会看到大量的不正常的状态,例如statistics正占用大量的时 间。这通常表示,某个地方有异常了,可以通过使用一些技巧来诊断到底是哪 个环节出现了问题。
查询缓存
在解析一个查询语句之前,如果查询缓存是打开的,那么MySQL会优先检查这个查询 是否命中查询缓存中的数据。这个检查是通过一个对大小写敏感的哈希查找实现的。查 询和缓存中的查询即使只有一个字节不同,那也不会匹配缓存结果, 这种情况下查询 就会进入下一阶段的处理。
如果当前的查询恰好命中了查询缓存,那么在返回查询结果之前MySQL会检查一次用 户权限。这仍然是无须解析查询SQL语句的,因为在查询缓存中已经存放了当前查询需 要访问的表信息。如果权限没有问题,MySQL会跳过所有其他阶段,直接从缓存中拿 到结果并返回给客户端。
这种情况下,查询不会被解析,不用生成执行计划,不会被执行。
语法解析器和预处理
查询的生命周期的下一步是将一个SQL转换成一个执行计划,MySQL再依照这个执行 计划和存储引擎进行交互。这包括多个子阶段:解析SQL、预处理、优化SQL执行计划。 这个过程中任何错误(例如语法错误)都可能终止查询。这里不打算详细介绍MySQL 内部实现,而只是选择性地介绍其中几个独立的部分,在实际执行中,这几部分可能一 起执行也可能单独执行。我们的目的是帮助大家理解MySQL如何执行查询,以便写出 更优秀的查询。
首先,MySQL通过关键字将SQL语句进行解析,并生成一棵对应的“解析树”。
MySQL解析器将使用MySQL语法规则验证和解析查询。例如,它将验证是否使用错误的关键字,或者使用关键字的顺序是否正确等,再或者它还会验证引号是否能前后正确 匹配。
预处理器则根据一些MySQL规则进一步检查解析树是否合法,例如,这里将检查数据 表和数据列是否存在,还会解析名字和别名,看看它们是否有歧义。下一步预处理器会验证权限。这通常很快,除非服务器上有非常多的权限配置。
查询优化器
现在语法树被认为是合法的了,并且由优化器将其转化成执行计划。一条查询可以有很 多种执行方式,最后都返回相同的结果。优化器的作用就是找到这其中最好的执行计划。
MySQL使用基于成本的优化器,它将尝试预测一个查询使用某种执行计划时的成本, 并选择其中成本最小的一个。最初,成本的最小单位是随机读取一个4K数据页的成本, 后来(成本计算公式)变得更加复杂,井且引入了一些“因子”来估算某些操作的代价, 如当执行一次WHERE条件比较的成本。可以通过查询当前会话的Last_query_cost的值 来得知MySQL计算的当前查询的成本。
这个结果表示MySQL的优化器认为大概需要做1040个数据页的随机查找才能完成上 面的查询。
这是根据一系列的统计信息计算得来的:每个表或者索引的页面个数、索引 的基数(索引中不同值的数量)、索引和数据行的长度、索引分布情况。优化器在评估 成本的时候并不考虑任何层面的缓存,它假设读取任何数据都需要一次磁盘I/O。
查询优化器不准确的原因
有很多种原因会导致MySQL优化器选择错误的执行计划,如下所示:
统计信息不准确。MySQL依赖存储引擎提供的统计信息来评估成本,但是有的存储 引擎提供的信息是准确的,有的偏差可能非常大。例如,InnoDB因为其MVCC的 架构,并不能维护一个数据表的行数的精确统计信息。
执行计划中的成本估算不等同于实际执行的成本。所以即使统计信息精准,优化器 给出的执行计划也可能不是最优的。例如有时候某个执行计划虽然需要读取更多的 页面,但是它的成本却更小。因为如果这些页面都是顺序读或者这些页面都已经在 内存中的话,那么它的访问成本将很小。MySQL层面并不知道哪些页面在内存中、 哪些在磁盘上,所以查询实际执行过程中到底需要多少次物理I/O是无法得知的。
MySQL的最优可能和你想的最优不一样。你可能希望执行时间尽可能的短,但是 MySQL只是基于其成本模型选择最优的执行计划,而有些时候这井不是最快的执行 方式。所以,这里我们看到根据执行成本来选择执行计划并不是完美的模型。
MySQL从不考虑其他并发执行的查询,这可能会影响到当前查询的速度。
MySQL也并不是任何时候都是基于成本的优化。有时也会基于一些固定的规则,例 如,如果存在全文搜索的MATCH()子句,则在存在全文索引的时候就使用全文索引。 即使有时候使用别的索引和WHERE条件可以远比这种方式要快,MySQL也仍然会使 用对应的全文索引。
MySQL不会考虑不受其控制的操作的成本,例如执行存储过程或者用户自定义函数 的成本。
后面我们还会看到,优化器有时候无法去估算所有可能的执行计划,所以它可能错过实际上最优的执行计划。
优化器的优化策略
MySQL的查询优化器是一个非常复杂的部件,它使用了很多优化策略来生成一个最优 的执行计划。优化策略可以简单地分为两种,一种是静态优化,一种是动态优化。
静态优化可以直接对解析树进行分析,并完成优化。例如,优化器可以通过一些简单的代数 变换将WHERE条件转换成另一种等价形式。静态优化不依赖于特别的数值,如WHERE条 件中带入的一些常数等。静态优化在第一次完成后就一直有效,即使使用不同的参数重 复执行查询也不会发生变化。可以认为这是一种“编译时优化”。
相反,动态优化则和查询的上下文有关,也可能和很多其他因素有关,例如WHERE条件 中的取值、索引中条目对应的数据行数等。这需要在每次查询的时候都重新评估,可以 认为这是“运行时优化”。
在执行语句和存储过程的时候,动态优化和静态优化的区别非常重要。MySQL对查询 的静态优化只需要做一次,但对查询的动态优化则在每次执行时都需要重新评估。有时 候甚至在查询的执行过程中也会重新优化。
优化器的优化类
下面是一些MySQL能够处理的优化类型:
重新定义关联表的顺序
数据表的关联井不总是按照在查询中指定的顺序进行。决定关联的顺序是优化器很 重要的一部分功能。
将外连接转化成内连接
并不是所有的OUTER JOIN语句都必须以外连接的方式执行。诸多因素,例如WHERE 条件、库表结构都可能会让外连接等价于一个内连接。MySQL能够识别这点并重写 查询,让其可以调整关联顺序。内连接的效率要高于外连接
使用等价变换规则
MySQL可以使用一些等价变换来简化并规范表达式。它可以合并和减少一些比较, 还可以移除一些恒成立和一些恒不成立的判断。
例如,(5=5 AND a>5)将被改写为 a>5。类似的,如果有(a<b AND b=c) AND a=5则会改写为b>5 AND b=c AND a=5。
这些规则对于我们编写条件语句很有用。
优化COUNT()、MIN()和MAX()
索引和列是否可为空通常可以帮助MySQL优化这类表达式。例如,要找到某一列 的最小值,只需要查询对应B-Tree索引最左端的记录,MySQL可以直接获取索引 的第一行记录。在优化器生成执行计划的时候就可以利用这一点,在B-Tree索引中, 优化器会将这个表达式作为一个常数对待。类似的,如果要查找一个最大值,也只 需读取B-Tree索引的最后一条记录。如果MySQL使用了这种类型的优化,那么在 EXPLAIN中就可以看到"Select tables Optimized away"。从字面意思可以看出,它表 示优化器已经从执行计划中移除了该表,并以一个常数取而代之。
类似的,没有任何WHERE条件的COUNT(*)查询通常也可以使用存储引擎提供的一些 优化(例如,MyISAM维护了一个变量来存放数据表的行数)。
预估并转化为常数表达式
当MySQL检测到一个表达式可以转化为常数的时候,就会一直把该表达式作为常 数进行优化处理。例如,一个用户自定义变量在查询中没有发生变化时就可以转换 为一个常数。数学表达式则是另一种典型的例子。
让人惊讶的是,在优化阶段,有时候甚至一个查询也能够转化为一个常数。一个例 子是在索引列上执行MIN()函数。甚至是主键或者唯一键查找语句也可以转换为常 数表达式。如果WHERE子句中使用了该类索引的常数条件,MySQL可以在查询开 始阶段就先查找到这些值,这样优化器就能够知道并转换为常数表达式。下面是一 个例子:
MySQL分两步来执行这个查询,也就是上面执行计划的两行输出。第一步先从film 表找到需要的行。因为在film_id字段上有主键索引,所以MySQL优化器知道这只 会返回一行数据,优化器在生成执行计划的时候,就已经通过索引信息知道将返回多少行数据。因为优化器已经明确知道有多少个值(WHERE条件中的值)需要做索 引查询,所以这里的表访问类型是const。
在执行计划的第二步,MySQL将第一步中返回的film_id列当作一个已知取值的列 来处理。因为优化器清楚在第一步执行完成后,该值就会是明确的了。注意到正如 第一步中一样,使用film_actor字段对表的访问类型也是const。
另一种会看到常数条件的情况是通过等式将常数值从一个表传到另一个表,这可以 通过WHERE、USING或者ON语句来限制某列取值为常数。在上面的例子中,因为使 用了USING子句,优化器知道这也限制了film_id在整个查询过程中都始终是一个常 量--因为它必须等于WHERE子句中的那个取值。
覆盖索引扫描
当索引中的列包含所有查询中需要使用的列的时候,MySQL就可以使用索引返回需 要的数据,而无须查询对应的数据行。
子查询优化
MySQL在某些情况下可以将子查询转换一种效率更高的形式,从而减少多个查询多次对数据进行访问。例如将经常查询的数据放入到缓存中
提前终止查询
在发现已经满足查询需求的时候,MySQL总是能够立刻终止查询。一个典型的例子 就是当使用了LIMIT子句的时候。除此之外,MySQL还有几类情况也会提前终止查 询,例如发现了一个不成立的条件,这时MySQL可以立刻返回一个空结果。从下 面的例子可以看到这一点:
从这个例子看到查询在优化阶段就已经终止。除此之外,M}'SQL在执行过程中,如 果发现某些特殊的条件,则会提前终止查询。当存储引擎需要检索”不同取值”或 者判断存在性的时候,M}'SQL都可以使用这类优化。例如,我们现在需要找到没有 演员的所有电影
这个查询将会过滤掉所有有演员的电影。每一部电影可能会有很多的演员,但是上面的查询一旦找到任何一个,就会停止并立刻判断下一部电影,因为只要有一名演员,
那么WHERE条件则会过滤掉这类电影。类似这种“不同值/不存在"的优化一般可用于DISTINCT、NOT EXIST()或者LEFT JOIN类型的查询。
等值传播
如果两个列的值通过等式关联,那么MySQL能够把其中一个列的WHERE条件传递 到另一列上。例如,我们看下面的查询:
因为这里使用了film_id字段进行等值关联,MySQL知道这里的WHERE子句不仅适 用于film表,而且对于film_actor表同样适用。如果使用的是其他的数据库管理系统, 可能还需要手动通过一些条件来告知优化器这个WHERE条件适用于两个表,那么写 法就会如下:
在MySQL中这是不必要的,这样写反而会让查询更难维护。
列表IN()的比较
在很多数据库系统中,IN()完全等同于多个OR条件的子句,因为这两者是完全等 价的。在MySQL中这点是不成立的,MySQL将IN()列表中的数据先进行排序, 然后通过二分查找的方式来确定列表中的值是否满足条件,这是一个O(log n)复杂 度的操作,等价地转换成OR查询的复杂度为O(n), 对于IN()列表中有大量取值的 时候,MySQL的处理速度将会更快。
上述小总结
上面列举的远不是MySQL优化器的全部,MySQL还会做大量其他的优化,但上面的这些例子已经足以让大家明白优化器的复杂性和智 能性了。如果说从上面这段讨论中我们应该学到什么,那就是“不要自以为比优化器更 聪明”。最终你可能会占点便宜,但是更有可能会使查询变得更加复杂而难以维护,而 最终的收益却为零。让优化器按照它的方式工作就可以了。
当然,虽然优化器已经很智能了,但是有时候也无法给出最优的结果。有时候你可能比 优化器更了解数据,例如,由于应用逻辑使得某些条件总是成立,还有时,优化器缺少 某种功能特性,如啥希索引,再如前面提到的,从优化器的执行成本角度评估出来的最 优执行计划,实际运行中可能比其他的执行计划更慢。
如果能够确认优化器给出的不是最佳选择,并且清楚背后的原理,那么也可以帮助优化 器做进一步的优化。例如,可以在查询中添加hint提示,也可以重写查询,或者重新设计更优的库表结构,或者添加更合适的索引。
数据和索引的统计信息
重新回忆一下 MySQL架构由多个层次组成。在服务器层有查询优化器,却没有 保存数据和索引的统计信息。统计信息由存储引擎实现,不同的存储引擎可能会存储不 同的统计信息(也可以按照不同的格式存储统计信息)。某些引擎,例如Archive引擎, 则根本就没有存储任何统计信息!
因为服务器层没有任何统计信息,所以MySQL查询优化器在生成查询的执行计划时, 需要向存储引擎获取相应的统计信息。存储引擎则提供给优化器对应的统计信息,包括: 每个表或者索引有多少个页面、每个表的每个索引的基数是多少、数据行和索引长度、 索引的分布信息等。优化器根据这些信息来选择一个最优的执行计划。在后面我们将看到统计信息是如何影响优化器的。
MySQL如何执行关联查询
MySQL中“关联”一词所包含的意义比一般意义上理解的要更广泛。总的来说, MySQL认为任何一个查询都是一次“关联”---井不仅仅是一个查询需要到两个表 匹配才叫关联,所以在MySQL中,每一个查询,每一个片段(包括子查询,甚至基 于单表的SELECT)都可能是关联。
所以,理解MySQL如何执行关联查询至关重要。我们先来看一个UNION查询的例子。 对于UNION查询,MySQL先将一系列的单个查询结果放到一个临时表中,然后再重新 读出临时表数据来完成UNION查询。在MySQL的概念中,每个查询都是一次关联,所 以读取结果临时表也是一次关联。
当前MySQL关联执行的策略很简单: MySQL对任何关联都执行嵌套循环关联操作,即 MySQL先在一个表中循环取出单条数据,然后再嵌套循环到下一个表中寻找匹配的行, 依次下去,直到找到所有表中匹配的行为止。
然后根据各个表匹配的行,返回查询中需 要的各个列。MySQL会尝试在最后一个关联表中找到所有匹配的行,如果最后一个关联表无法找到更多的行以后,MySQL返回到上一层次关联表,看是否能够找到更多的 匹配记录,依此类推迭代执行。
按照这样的方式查找第一个表记录,再嵌套查询下一个关联表,然后回溯到上一个表, 在MySQL中是通过嵌套循环的方式实现一正如其名"嵌套循环关联"。请看下面的例 子中的简单查询:
假设MySQL按照查询中的表顺序进行关联操作,我们则可以用下面的伪代码表示 MySQL将如何完成这个查询:
上面的执行计划对于单表查询和多表关联查询都适用,如果是一个单表查询,那么只需 完成上面外层的基本操作。对于外连接上面的执行过程仍然适用。例如,我们将上面查 询修改如下:
另一种可视化查询执行计划的方法是根据优化器执行的路径绘制出对应的"泳道图”。如 图所示,绘制了前面示例中内连接的泳道图,请从左至右,从上至下地看这幅图。
从本质上说,MySQL对所有的类型的查询都以同样的方式运行。例如,MySQL在FROM 子句中遇到子查询时,先执行子查询井将其结果放到一个临时表中, 然后将这个临时 表当作一个普通表对待(正如其名“派生表")。
MySQL在执行UNION查询时也使用类 似的临时表,在遇到右外连接的时候,MySQL将其改写成等价的左外连接。简而言之, 当前版本的MySQL会将所有的查询类型都转换成类似的执行计划。
不过,不是所有的查询都可以转换成上面的形式。例如,全外连接就无法通过嵌套循环 和回溯的方式完成,这时当发现关联表中没有找到任何匹配行的时候,则可能是因为关 联是恰好从一个没有任何匹配的表开始。这大概也是MySQL并不支持全外连接的原因。 还有些场景,虽然可以转换成嵌套循环的方式,但是效率却非常差,后面我们会看一个 这样的例子。
执行计划
和很多其他关系数据库不同,MySQL并不会生成查询字节码来执行查询。MySQL生成 查询的一棵指令树,然后通过存储引擎执行完成这棵指令树并返回结果。最终的执行计 划包含了重构查询的全部信息。如果对某个查询执行EXPLAIN EXTENDED后,再执行SHOW WARtJINGS, 就可以看到重构出的查询。
任何多表查询都可以使用一棵树表示,例如,可以按照图执行一个四表的关联操作。
在计算机科学中,这被称为一颗平衡树。但是,这并不是MySQL执行查询的方式。正 如前面介绍的,MySQL总是从一个表开始一直嵌套循环、回溯完成所有表关联。 所以,MySQL的执行计划总是如图所示,是一棵左测深度优先的树。
关联查询优化器与关联查询的实现方式(重点)
MySQL优化器最重要的一部分就是关联查询优化,它决定了多个表关联时的顺序。通 常多表关联的时候,可以有多种不同的关联顺序来获得相同的执行结果。
mysql的关联查询很重要,但其实关联查询执行的策略比较简单:
mysql对任何关联都执行嵌套循环关联操作,即mysql先在一张表中循环取出单条数据,然后再嵌套到下一个表中寻找匹配的行,依次下去,直到找到所有表中匹配的行为止。然后根据各个表匹配的行,返回查询中需要的各个列。mysql会尝试再最后一个关联表中找到所有匹配的行,如果最后一个关联表无法找到更多的行之后,mysql返回到上一层次关联表,看是否能够找到更多的匹配记录,以此类推迭代执行。整体的思路如此,但是要注意实际的执行过程中有多个变种形式:
join的实现方式原理
Simple Nested-Loop Join
相当于r,取出一行,匹配s
Index Nested-Loop Join
相当于r,取出一行,匹配s,但是s上有普通索引(需要回表)或者主键索引,每次匹配查询的数据很少
Block Nested-Loop Join
相当于s没有索引,但是将列上所有的数据,放入缓冲区,组成类似于索引的东西。
然后r,取出一行,匹配buffer,buffer里查询的速度很快。
1 Join Buffer会缓存所有参与查询的列而不是只有Join的列。
2 可以通过调整join_buffer_size缓存大小
3 join_buffer_size的默认值是256K,join_buffer_size的最大值在MySQL 5.1.22版本前是4G-1,而之后的版本才能在64位操作系统下申请大于4G的Join Buffer空间。
4 使用Block Nested-Loop Join算法需要开启优化器管理配置的optimizer_switch的设置block_nested_loop为on,默认为开启。
show variables like '%optimizer_switch%'
关联查询优化 器则通过评估不同顺序时的成本来选择一个代价最小的关联顺序。 下面的查询可以通过不同顺序的关联最后都获得相同的结果:
容易看出,可以通过一些不同的执行计划来完成上面的查询。例如,MySQL可以从 film表开始,使用film_actor表的索引film_id来查找对应的actor_id值,然后再根据 actor表的主键找到对应的记录。Oracle用户会用下面的术语描述: "film表作为驱动表 先查找file_actor表,然后以此结果为驱动表再查找actor表”。这样做效率应该会不错, 我们再使用EXPLAIN看看MySQL将如何执行这个查询:
这和我们前面给出的执行计划完全不同。MySQL从actor表开始(我们从上面的 EXPLAIN结果的第一行输出可以看出这点),然后与我们前面的计划按照相反的顺序进行 关联。这样是否效率更高呢?我们来看看,我们先使用STRAIGHT_JOIN关键字,按照我 们之前的顺序执行,这里是对应的EXPLAIN输出结果:
我们来分析一下为什么MySQL会将关联顺序倒转过来:可以看到,关联顺序倒转后的 第一个关联表只需要扫描很少的行数。在两种关联顺序下,第二个和第三个关联表都 是根据索引查询,速度都很快,不同的是需要扫描的索引项的数量是不同的:
将film表作为第一个关联表时,会找到951条记录,然后对film_actor和actor表 进行嵌套循环查询。
如果MySQL选择首先扫描actor表,只会返回200条记录进行后面的嵌套循环查询。
换句话说,倒转的关联顺序会让查询进行更少的嵌套循环和回溯操作。为了验证优化器 的选择是否正确,我们单独执行这两个查询,并且看看对应的Last_query_cost状态值。 我们看到倒转的关联顺序的预估成本为241, 而原来的查询的预估成本为1 154。
这个简单的例子主要想说明MySQL是如何选择合适的关联顺序来让查询执行的成本尽 可能低的。重新定义关联的顺序是优化器非常重要的一部分功能。不过有的时候,优化器给出的并不是最优的关联顺序。这时可以使用STRAIGHT_JOIN关键字重写查询,让优 化器按照你认为的最优的关联顺序执行。不过老实说,人的判断很难那么精准。绝大 多数时候,优化器做出的选择都比普通人的判断要更准确。
关联优化器会尝试在所有的关联顺序中选择一个成本最小的来生成执行计划树。如果可 能,优化器会遍历每一个表然后逐个做嵌套循环计算每一棵可能的执行计划树的成本, 最后返回一个最优的执行计划。
不过,糟糕的是,如果有超过n个表的关联,那么需要检查n的阶乘种关联顺序。我们 称之为所有可能的执行计划的"搜索空间”,搜索空间的增长速度非常快。例如,若 是10个表的关联,那么共有3 628 800种不同的关联顺序!当搜索空间非常大的时候, 优化器不可能逐一评估每一种关联顺序的成本。这时,优化器选择使用“贪婪"搜索的 方式查找“最优”的关联顺序。实际上,当需要关联的表超过optimizer_ search_ depth 的限制的时候,就会选择“贪婪"搜索模式了(optimizer _search_ depth参数可以根据 需要指定大小)。
在MySQL这些年的发展过程中,优化器积累了很多“启发式”的优化策略来加速执行 计划的生成。绝大多数情况下,这都是有效的,但因为不会去计算每一种关联顺序的成本, 所以偶尔也会选择一个不是最优的执行计划。
有时,各个查询的顺序并不能随意安排,这时关联优化器可以根据这些规则大大减少搜 索空间,例如,左连接、相关子查询(后面我将继续讨论子查询)。这是因为,后面的 表的查询需要依赖于前面表的查询结果。这种依赖关系通常可以帮助优化器大大减少需 要扫描的执行计划数量。
排序优化
无论如何排序都是一个成本很高的操作,所以从性能角度考虑,应尽可能避免排序或者 尽可能避免对大量数据进行排序。
我们已经看到MySQL如何通过索引进行排序。当不能使用索引生成排序结果的时候,MySQL需要自已进行排序,如果数据量小则在内存中进行,如果数据量大 则需要使用磁盘,不过MySQL将这个过程统一称为文件排序(filesort) , 即使完全是内 存排序不需要任何磁盘文件时也是如此。
如果需要排序的数据量小于排序缓冲区(show variables like '%sort_buffer_size%';),mysql使用内存进行快速排序操作,如果内存不够排序,那么mysql就会先将树分块,对每个独立的块使用快速排序进行排序,并将各个块的排序结果存放再磁盘上,然后将各个排好序的块进行合并,最后返回排序结果
MySQL有如下两种排序算法:
两次传输排序(旧版本使用)
读取行指针和需要排序的字段,对其进行排序,然后再根据排序结果读取所需要的 数据行。
例如order by time,先去读取所有的id和time(遍历),然后对time进行排序,然后根据id去表里查数据(每个id就是一个随机IO)
这需要进行两次数据传输,即需要从数据表中读取两次数据,第二次读取数据的时 候,因为是读取排序列进行排序后的所有记录,这会产生大量的随机I/O, 所以两 次数据传输的成本非常高。当使用的是MyISAM表的时候,成本可能会更高,因为 MyISAM使用系统调用进行数据的读取(MylSAM非常依赖操作系统对数据的缓存)。不过这样做的优点是,在排序的时候存储尽可能少的数据,这就让"排序缓冲区”中可能容纳尽可能多的行数进行排序。
单次传输排序(新版本使用)
先读取查询所需要的所有列,然后再根据给定列进行排序,最后直接返回排序结果。
例如select name ,order by time,先去读取name和time,然后根据time排序,完事
这个算法只在MySQL 4.1和后续更新的版本才引人。因为不再需要从数据表中读取 两次数据,对于I/O密集型的应用,这样做的效率高了很多。另外,相比两次传输排序, 这个算法只需要一次顺序I/O读取所有的数据,而无须任何的随机I/O。缺点是,如 果需要返回的列非常多、非常大,会额外占用大量的空间,而这些列对排序操作本 身来说是没有任何作用的。因为单条排序记录很大,所以可能会有更多的排序块需 要合并。
很难说哪个算法效率更高,两种算法都有各自最好和最糟的场景。当查询需要所有列的总长度不超过参数max_length_for_sort_data时,MySQL使用“单次传输排 序”,可以通过调整这个参数来影响MySQL排序算法的选择。
MySQL在进行文件排序的时候需要使用的临时存储空间可能会比想象的要大得多。原 因在于MySQL在排序时,对每一个排序记录都会分配一个足够长的定长空间来存放。
这个定长空间必须足够长以容纳其中最长的字符串,例如,如果是VARCHAR列则需要分 配其完整长度,如果使用UTF-8字符集,那么MySQL将会为每个字符预留三个字节。 我们曾经在一个库表结构设计不合理的案例中看到,排序消耗的临时空间比磁盘上的原 表要大很多倍。
在关联查询的时候如果需要排序,MySQL会分两种情况来处理这样的文件排序。
如 果ORDER BY子句中的所有列都来自关联的第一个表,那么MySQL在关联处理第一 个表的时候就进行文件排序。如果是这样,那么在MySQL的EXPLAIN结果中可以看到 Extra字段会有"Using filesort"。
除此之外的所有情况,MySQL都会先将关联的结果 存放到一个临时表中,然后在所有的关联都结束后,再进行文件排序。这种情况下,在 MySQL的EXPLAIN结果的Extra字段可以看到"Using temporary; Using filesort"。
如果 查询中有LIMIT的话,LIMIT也会在排序之后应用,所以即使需要返回较少的数据,临 时表和需要排序的数据量仍然会非常大。
MySQL 5.6在这里做了很多重要的改进。当只需要返回部分排序结果的时候,例如使用 了LIMIT子句,MySQL不再对所有的结果进行排序,而是根据实际情况,选择抛弃不 满足条件的结果,然后再进行排序。
查询执行引擎
在解析和优化阶段,MySQL将生成查询对应的执行计划,MySQL的查询执行引擎则根 据这个执行计划来完成整个查询。这里执行计划是一个数据结构,而不是和很多其他的 关系型数据库那样会生成对应的字节码。
相对于查询优化阶段,查询执行阶段不是那么复杂: MySQL只是简单地根据执行计划 给出的指令逐步执行。在根据执行计划逐步执行的过程中,有大量的操作需要通过调用 存储引擎实现的接口来完成,这些接口也就是我们称为"handler API"的接口。
查询中 的每一个表由一个handler的实例表示。前面我们有意忽略了这点,实际上,MySQL在 优化阶段就为每个表创建了一个handler实例,优化器根据这些实例的接口可以获取表 的相关信息,包括表的所有列名、索引统计信息,等等。
存储引擎接口有着非常丰富的功能,但是底层接口却只有几十个,这些接口像"搭积木” 一样能够完成查询的大部分操作。例如,有一个查询某个索引的第一行的接口,再有一 个查询某个索引条目的下一个条目的功能,有了这两个功能我们就可以完成全索引扫描 的操作了。这种简单的接口模式,让MySQL的存储引擎插件式架构成为可能,但是正 如前面的讨论,也给优化器带来了一定的限制。
井不是所有的操作都由handler完成。
例如,当MySQL需要进行表锁的时候, 可能会实现自己的级别的、更细粒度的锁,如InnoDB就实现了自己的行锁,但这并不能代替服务器层的表锁。
如果是所有存 储引擎共有的特性,则由服务器层实现,比如时间和日期函数、视图、触发器等。
为了执行查询,MySQL只需要重复执行计划中的各个操作,直到完成所有的数据查询。
返回结果给客户端
查询执行的最后一个阶段是将结果返回给客户端。即使查询不需要返回结果集给客户端, MySQL仍然会返回这个查询的一些信息,如该查询影响到的行数。
如果查询可以被缓存,那么MySQL在这个阶段也会将结果存放到查询缓存中。
MySQL将结果集返回客户端是一个增量、逐步返回的过程。例如,我们回头看看前面 的关联操作,一旦服务器处理完最后一个关联表,开始生成第一条结果时,MySQL就 可以开始向客户端逐步返回结果集了。
这样处理有两个好处:服务器端无须存储太多的结果,也就不会因为要返回太多结果而 消耗太多内存。另外,这样的处理也让MySQL客户端第一时间获得返回的结果
结果集中的每一行都会以一个满足MySQL客户端/服务器通信协议的封包发送,再通 过TCP协议进行传输,在TCP传输的过程中,可能对MySQL的封包进行缓存然后批 量传输。
MySQL查询优化器的局限性
.MySQL的万能“嵌套循环”并不是对每种查询都是最优的。不过还好,MySQL查询优 化器只对少部分查询不适用,而且我们往往可以通过改写查询让MySQL高效地完成工 作。还有一个好消息,MySQL 5.6版本正式发布后,会消除很多MySQL原本的限制, 让更多的查询能够以尽可能高的效率完成。
关联子查询
MYSQL的子查询实现得非常糟糕。最糟糕的一类查询是WHERE条件中包含IN()的子查 询语句。
例如,我们希望找到Sakila数据库中,演员Penelope Guiness (他的actor_id 为1)参演过的所有影片信息。很自然的,我们会按照下面的方式用子查询实现:
因为MYSQL对IN()列表中的选项有专门的优化策略,一般会认为MYSQL会先执行子 查询返回所有包含actor_id为1的film_id。一般来说,IN()列表查询速度很快,所以 我们会认为上面的查询会这样执行:
很不幸,MySQL不是这样做的。MySQL会将相关的外层表压到子查询中,它认为这样 可以更高效率地查找到数据行。也就是说,MySQL会将查询改写成下面的样子:
这时,子查询需要根据film_id来关联外部表film, 因为需要film_id字段,所以MySQL 认为无法先执行这个子查询。通过EXPLAIN我们可以看到子查询是一个相关子查询 (DEPENDENT SUBQUERY) (可以使用EXPLAIN EXTENDED来查看这个查询被改写成了什么 样子):
根据EXPLAIN的输出我们可以看到,MySQL先选择对file表进行全表扫描,然后根据 返回的film_id 逐个执行子查询。如果是一个很小的表,这个查询糟糕的性能可能还不会 引起注意,但是如果外层的表是一个非常大的表,那么这个查询的性能会非常糟糕。当 然我们很容易用下面的办法来重写这个查询:
另一个优化的办法是使用函数GROUP_ CONCAT ()在IN()中构造一个由逗号分隔的列表。 有时这比上面的使用关联改写更快。因为使用IN()加子查询,性能经常会非常糟,所以 通常建议使用EXISTS()等效的改写查询来获取更好的效率。下面是另一种改写IN()加子查询的办法:
UNION的限制
有时,MySQL无法将限制条件从外层”下推”到内层,这使得原本能够限制部分返回 结果的条件无法应用到内层查询的优化上。
如果希望UNION的各个子句能够根据LIMIT只取部分结果集,或者希望能够先排好序再 合井结果集的话,就需要在UNION的各个子句中分别使用这些子句。
例如,想将两个子 查询结果联合起来,然后再取前20条记录,那么MySQL会将两个表都存放到同一个临 时表中,然后再取出前20行记录:
这条查询将会把actor中的200条记录和customer表中的599条记录存放在一个临时 表中,然后再从临时表中取出前20条。可以通过在UNION的两个子查询中分别加上一个 LIMIT 20来减少临时表中的数据:
现在中间的临时表只会包含40条记录了,除了性能考虑之外,在这里还需要注意一点: 从临时表中取出数据的顺序并不是一定的,所以如果想获得正确的顺序,还需要加上一 个全局的ORDER BY和LIMIT操作。
索引合并优化
在5.0和更新的版本中,当WHERE子句中包含多个复杂条件的 时候,MySQL能够访问单个表的多个索引以合并和交叉过滤的方式来定位需要查找的行。
等值传递
某些时候,等值传递会带来一些意想不到的额外消耗。例如,有一个非常大的IN()列表, 而MySQL优化器发现存在'WHERE、ON或者USING的子句,将这个列表的值和另一个表 的某个列相关联。
那么优化器会将IN()列表都复制应用到关联的各个表中。通常,因为各个表新增了过滤 条件,优化器可以更高效地从存储引擎过滤记录。但是如果这个列表非常大,则会导致 优化和执行都会变慢。在本书写作的时候,除了修改MySQL源代码,目前还没有什么 办法能够绕过该问题(不过这个问题很少会碰到)。
并行执行
MySQL无法利用多核特性来并行执行查询。很多其他的关系型数据库能够提供这个特 性,但是MySQL做不到。这里特别指出是想告诉不要花时间去尝试寻找并行执行 查询的方法。
哈希关联
在本书写作的时候,MySQL并不支持哈希关联--MySQL的所有关联都是嵌套循环关 联。
不过,可以通过建立一个哈希索引来曲线地实现哈希关联。
如果使用的是Memory 存储引擎,则索引都是哈希索引,所以关联的时候也类似于哈希关联。
可以参考“创建自定义哈希索引”。
另外,MariaDB已经实现了真正的哈希关联。
松散索引扫描
由于历史原因,MySQL并不支持松散索引扫描,也就无法按照不连续的方式扫描一个 索引。通常,MySQL的索引扫描需要先定义一个起点和终点,即使需要的数据只是这 段索引中很少数的几个,MySQL仍需要扫描这段索引中每一个条目。
下面我们通过一个示例说明这点。假设我们有如下索引(a, b), 有下面的查询:
mysql> SELECT…FROM tbl WHERE b BETWEEN 2 AND 3;
因为索引的前导字段是列a, 但是在查询中只指定了字段b, MySQL无法使用这个索引, 从而只能通过全表扫描找到匹配的行,如图
了解索引的物理结构的话,不难发现还可以有一个更快的办法执行上面的查询。索引的 物理结构(不是存储引擎的API)使得可以先扫描a列第一个值对应的b列的范围,然 后再跳到a列第二个不同值扫描对应的b列的范围。图展示了如果由MySQL来实 现这个过程会怎样。
注意到,这时就无须再使用WHERE子句过滤,因为松散索引扫描已经跳过了所有不需要 的记录。
上面是一个简单的例子,除了松散索引扫描,新增一个合适的索引当然也可以优化上述 查询。但对于某些场景,增加索引是没用的,例如,对于第一个索引列是范围条件,第 二个索引列是等值条件的查询,靠增加索引就无法解决问题。
MySQL 5.0之后的版本,在某些特殊的场景下是可以使用松散索引扫描的,例如,在一 个分组查询中需要找到分组的最大值和最小值:
在EXPLAIN中的Extra字段显示"Using index for group-by", 表示这里将使用松散索引 扫描,不过如果MySQL能写上"loose index probe", 相信会更好理解。
在MySQL很好地支持松散索引扫描之前,一个简单的绕过问题的办法就是给前面的列加上可能的常数值。
在MySQL5.6之后的版本,关于松散索引扫描的一些限制将会通过“索引条件下推(index condition pushdown)"的方式解决。
最大值和最小值优化
对于MIN()和MAX()查询,MySQL的优化做得并不好。这里有一个例子:
mysql> SELECT MIN(actor_id) FROM sakila.actor WHERE first_name ='PENELOPE';
因为在first_name字段上并没有索引,因此MySQL将会进行一次全表扫描。如果 MySQL能够进行主键扫描,那么理论上,当MySQL读到第一个满足条件的记录的时候, 就是我们需要找的最小值了,因为主键是严格按照actor_id字段的大小顺序排列的。但 是MySQL这时只会做全表扫描,我们可以通过查看SHOW STATUS的全表扫描计数器来 验证这一点。一个曲线的优化办法是移除MIN(), 然后使用LIMIT来将查询重写如下:
这个策略可以让MySQL扫描尽可能少的记录数。如果你是一个完美主义者,可能会说 这个SQL已经无法表达她的本意了。一般我们通过SQL告诉服务器我们需要什么数据, 由服务器来决定如何最优地获取数据,不过在这个案例中,我们其实是告诉MySQL如 何去获取我们需要的数据,通过SQL井不能一眼就看出我们其实是想要一个最小值。确 实如此,有时候为了获得更高的性能,我们不得不放弃一些原则。
在同一个表上查询和更新
MySQL不允许对同一张表同时进行查询和更新。这其实井不是优化器的限制,如果清 楚MySQL是如何执行查询的,就可以避免这种情况。下面是一个无法运行的SQL, 虽 然这是一个符合标准的SQL语句。这个SQL语句尝试将两个表中相似行的数量记录到 字段cnt中:
可以通过使用生成表的形式来绕过上面的限制,因为MySQL只会把这个表当作一个临 时表来处理。实际上,这执行了两个查询:一个是子查询中的SELECT语句,另一个是多 表关联UPDATE, 只是关联的表是一个临时表。子查询会在UPDATE语句打开表之前就完成, 所以下面的查询将会正常执行:
查询优化器的局限性总结
如果希望UNION的各个子句能够根据LIMIT只取部分结果集,或者希望能够先排好序再 合井结果集的话,就需要在UNION的各个子句中分别使用这些子句的LIMIT,然后合并后再LIMIT(如20+20 limit 20)。
当WHERE子句中包含多个复杂条件的 时候,MySQL能够访问单个表的多个索引以合并和交叉过滤的方式来定位需要查找的行。
在一 个分组查询中需要找到分组的最大值和最小值,或者给前面的列加上可能的常数值,可以使用松散索引。(索引(a,b)对b范围查询,或者找b的max/min)
如果对主键,或者其他索引字段,进行min的查询,当有where条件时,可以使用索引,然后limit 1
在同一张表进行查询和更新,可以通过生成一张临时表,然后进行修改。
查询优化器的提示(hint)
如果对优化器选择的执行计划不满意,可以使用优化器提供的几个提示(hint)来控制 最终的执行计划。下面将列举一些常见的提示,并简单地给出什么时候使用该提示。通 过在查询中加入相应的提示,就可以控制该查询的执行计划。关于每个提示的具体用法, 建议直接阅读MySQL官方手册。有些提示和版本有直接关系。可以使用的一些提示如 下:
HIGH_PRIORITY和LOW_PRIORITY
这个提示告诉MySQL, 当多个语句同时访问某一个表的时候,哪些语句的优先级相 对高些、哪些语句的优先级相对低些。
HIGH_PRIORITY用于SELECT语句的时候,MySQL会将此SELECT语句重新调度到所 有正在等待表锁以便修改数据的语句之前。实际上MySQL是将其放在表的队列的 最前面,而不是按照常规顺序等待。HIGH_PRIORITY还可以用于INSERT语句,其效 果只是简单地抵消了全局LOW_ PRIORITY设置对该语句的影响。
LOW_PRIORITY则正好相反:它会让该语句一直处于等待状态,只要队列中还有需要 访问同一个表的语句--即使是那些比该语句还晚提交到服务器的语句。这就像一 个过于礼貌的人站在餐厅门口,只要还有其他顾客在等待就一直不进去,很明显这 容易把自己给饿坏。LOW_PRIORITY提示在SELECT、INSERT、UPDATE和DELETE语句 中都可以使用。
这两个提示只对使用表锁的存储引擎有效,千万不要在InnoDB或者其他有细粒度 锁机制和并发控制的引擎中使用。即使是在MylSAM中使用也要注意,因为这两个 提示会导致并发插入被禁用,可能会严重降低性能。
HIGH_PRIORITY和LOW_PRIORITY经常让人感到困惑。这两个提示并不会获取更多资 源让查询“积极”工作,也不会少获取资源让查询“消极”工作。它们只是简单地 控制了MySQL访问某个数据表的队列顺序。
DELAYED
这个提示对INSERT和REPLACE有效。MySQL会将使用该提示的语句立即返回给客 户端,并将插入的行数据放入到缓冲区,然后在表空闲时批量将数据写入。日志系 统使用这样的提示非常有效,或者是其他需要写入大量数据但是客户端却不需要等 待单条语句完成I/O的应用。
这个用法有一些限制:并不是所有的存储引擎都支持 这样的做法,并且该提示会导致函数LAST_ INSERT_ ID ()无法正常工作。
STRAIGHT_JOIN
这个提示可以放置在SELECT语句的SELECT关键字之后,也可以放置在任何两个关 联表的名字之间。
第一个用法是让查询中所有的表按照在语句中出现的顺序进行关 联。
第二个用法则是固定其前后两个表的关联顺序。
当MySQL没能选择正确的关联顺序的时候,或者由于可能的顺序太多导致MySQL 无法评估所有的关联顺序的时候,STRAIGHT _JOIN都会很有用。在后面这种情况, MySQL可能会花费大量时间在"statistics"状态,加上这个提示则会大大减少优化 器的搜索空间。
可以先使用EXPLAIN语句来查看优化器选择的关联顺序,然后使用该提示来重写 查询,再看看它的关联顺序。当你确定无论怎样的where条件,某个固定的关联 顺序始终是最佳的时候,使用这个提示可以大大提高优化器的效率。但是在升级 MySQL版本的时候,需要重新审视下这类查询,某些新的优化特性可能会因为该提 示而失效。
SQL_SMALL_RESULT和SQL_BIG_RESULT
这两个提示只对SELECT语句有效。它们告诉优化器对GROUP BY或者DISTINCT查询 如何使用临时表及排序。SQL_SMALL_RESULT告诉优化器结果集会很小,可以将结果 集放在内存中的索引临时表,以避免排序操作。
如果是SQL_BIG_RESULT, 则告诉优 化器结果集可能会非常大,建议使用磁盘临时表做排序操作。
SQL _BUFFER_ RESULT
这个提示告诉优化器将查询结果放入到一个临时表,然后尽可能快地释放表锁。
这 和前面提到的由客户端缓存结果不同。当你没法使用客户端缓存的时候,使用服务 器端的缓存通常很有效。带来的好处是无须在客户端上消耗太多的内存,还可以尽 可能快地释放对应的表锁。代价是,服务器端将需要更多的内存。
SQL_CACHE和SQL_NO_CACHE
这个提示告诉MySQL这个结果集是否应该缓存在查询缓存中。
SQL _ CALC _FOUND_ ROWS
严格来说,这并不是一个优化器提示。它不会告诉优化器任何关于执行计划的东西。 它会让MySQL返回的结果集包含更多的信息。查询中加上该提示MySQL会计算除去LIMIT子句后这个查询要返回的结果集的总数,而实际上只返回LIMIT要求的结果集。可以通过函数FOUND_ ROW ()获得这个值。(参阅后面的"SQL _ CALC _FOUND_ ROWS优化”部分,了解下为什么不应该使用该提示。)
FOR UPDATE和LOCK IN SHARE MODE
这也不是真正的优化器提示。这两个提示主要控制SELECT语句的锁机制,但只对 实现了行级锁的存储引擎有效。使用该提示会对符合查询条件的数据行加锁。
对于 INSERT ••• SELECT语句是不需要这两个提示的,因为对于MySQL 5.0和更新版本会 默认给这些记录加上读锁。(可以禁用该默认行为,但不是个好主意,在后面关于复 制和备份的章节中将解释这一点。)
唯一内置的支持这两个提示的引擎就是InnoDB。另外需要记住的是,这两个提示会 让某些优化无法正常使用,例如索引覆盖扫描。InnoDB不能在不访问主键的情况下 排他地锁定行,因为行的版本信息保存在主键中。
糟糕的是,这两个提示经常被滥用,很容易造成服务器的锁争用问题,后面章节我们将讨论这点。应该尽可能地避免使用这两个提示,通常都有其他更好的方式可以 实现同样的目的。
USE INDEX、IGNORE INDEX和FORCE INDEX
这几个提示会告诉优化器使用或者不使用哪些索引来查询记录(例如,在决定关联 顺序的时候使用哪个索引)。在MySQL 5.0和更早的版本,这些提示并不会影响到 优化器选择哪个索引进行排序和分组,在MyQL 5.1和之后的版本可以通过新增选 项FOR ORDER BY和FOR GROUP BY来指定是否对排序和分组有效。
FORCE INDEX和USE INDEX基本相同,除了一点: FORCE INDEX会告诉优化器全表扫 描的成本会远远高于索引扫描,哪怕实际上该索引用处不大。当发现优化器选择了 错误的索引,或者因为某些原因(比如在不使用ORDER BY的时候希望结果有序)要 使用另一个索引时,可以使用该提示。在前面关于如何使用LIMIT高效地获取最小 值的案例中,已经演示过这种用法。
在MySQL 5.0和更新版本中,新增了一些参数用来控制优化器的行为:
optimizer_ search_ depth
这个参数控制优化器在穷举执行计划时的限度。如果查询长时间处于"Statistics" 状态,那么可以考虑调低此参数。
optimizer_prune_level
该参数默认是打开的,这让优化器会根据需要扫描的行数来决定是否跳过某些执行 计划。
optimizer_switch
这个变量包含了一些开启/关闭优化器特性的标志位。例如在MySQL 5.1中可以通过这个参数来控制禁用索引合并的特性。
前两个参数是用来控制优化器可以走的一些“捷径"。这些捷径可以让优化器在处理非 常复杂的SQL语句时,仍然可以很高效,但这也可能让优化器错过一些真正最优的执行 计划。所以应该根据实际需要来修改这些参数。
MySQL升级后的验证
在优化器面前耍一些“小聪明”是不好的。这样做收效甚小,但是却给维护带来了 很多额外的工作量。在MySQL版本升级的时候,这个问题就很突出了,你设置的 “优化器提示“很可能会让新版的优化策略失效。
MySQL 5.0版本引入了大量优化策略,在还没有正式发布的5.6版本中,优化器的 改进也是近些年来最大的一次改进。如果要更新到这些版本,当然希望能够从这些 改进中受益。
新版MySQL基本上在各个方面都有非常大的改进,5.5和5.6这两个版本尤为突 出。升级操作一般来说都很顺利,但仍然建议仔细检查各个细节,以防止一些边界 情况影响你的应用程序。不过还好,要避免这些,你不需要付出太多的精力。使用 Percona Toolkit中的pt-upgrade工具,就可以检查在新版本中运行的SQL是否与 老版本一样,返回相同的结果。