如何设计最优的库表结构、 如何创建最好的索引, 这些对于高性能来讲是必不可少的。 但这些还不够一一还须要合理的设计查询。 若是查询写得很糟糕,即便库表结构再合理、 索引再合适, 也没法实现高性能。mysql
查询优化、 索引优化、 库表结构优化须要齐头井进, 一个不落。算法
若是把查询看做是一个任务, 那么它由一系列子任务组成, 每一个子任务都会消耗必定的时间。 若是要优化查询, 实际上要优化其子任务, 要么消除其中一些子任务, 要么减小子任务的执行次数,要么让子任务运行得更快。sql
查询的生命周期:从客户端,到服务器,而后在服务器上进行解析,生成执行计划,执行(包括调用存储引擎及调用后的排序、分组等数据处理),并返回结果给客户端。其中“执行”能够认为是整个生命周期中最重要的阶段,这其中包括了大量为了检索数据到存储引擎的调用以及调用后的数据处理,包括排序、分组等。数据库
在完成这些任务时,查询须要在不一样地方花费时间,包括网络,CPU 计算,生成统计信息和执行计划、锁等待(互斥等待)等操做,尤为是向底层存储引擎检索数据的调用操做,这些调用须要在内存操做、CPU操做和内存不足时致使的I/O操做上消耗时间。跟觉存储引擎不一样,可能还会产生大量的上下文切换以及系统调用。缓存
优化查询的目的就是减小和消除某些操做所话费的时间。服务器
了解查询的生命周期、清楚查询的时间消耗状况对于优化查询有很大的意义。网络
查询性能低下最基本的缘由是访问的数据太多。某些查询可能不可避免地须要筛选大量数据, 但这井不常见。大部分性能低下的查询均可以经过减小访问的数据量的方式进行优化。对于低效的查询, 咱们发现经过下面两个步骤来分析老是颇有效:数据结构
有些查询会请求超过实际须要的数据, 而后这些多余的数据会被应用程序丢弃。这会给MySQL服务器带来额外的负担, 并增长网络开销。另外也会消耗应用服务器的CPU 和内存资源。 一些经典案例:架构
SELECT * FROM sakila.actor INNER JOIN sakila.film_actor USING(actor_id) INNER JOIN sakila.film USING(film_id) where sakila.film.title = 'Academy Dinosaur';
正确的方式应该是像下面这样只取须要的列:并发
SELECT sakila.actor.* FROM sakila.actor ....;
在肯定查询只返回须要的数据之后,接下来应该看看查询为了返回结果是否扫描了过多 的数据。对于MySQL, 最简单的衡量查询开销的三个指标以下:
没有哪一个指标可以完美地衡量查询的开销,但它们大体反映了MySQL在内部执行查询时须要访问多少数据,并能够大概推算出查询运行的时间。 这三个指标都会记录到MySQL的慢日志中,因此检查慢日志记录是找出扫描行数过多的查询的好办法。
响应时间
响应时间是两个部分之和:服务时间和排队时间。 服务时间是指数据库处理这个查询真正花了多长时间。 排队时间是指服务器由于等待某些资源而没有真正执行查询的时间——多是等I/O操做完成,也多是等待行锁,等等。遗憾的是,咱们没法把响应 时间细分到上面这些部分,除非有什么办法可以逐个测量上面这些消耗,不过很难作到。 通常最多见和重要的等待是I/O和锁等待,可是实际状况更加复杂。
扫描的行数和返回的行数
分析查询时,查看该查询扫描的行数是很是有帮助的。 这在必定程度上可以说明该查询找到须要的数据的效率高不高。
对于找出那些 “糟糕” 的查询,这个指标可能还不够完美, 由于并非全部的行的访问代价都是相同的。 较短的行的访问速度更快, 内存中的行也比磁盘中的行的访问速度要快得多。
理想状况下扫描的行数和返回的行数应该是相同的。 但实际状况中这种 “美事” 井很少。例如在作一个关联查询时,服务器必需要扫描多行才能生成结果集中的一行。 扫描的行数对返回的行数的比率一般很小,通常在1:1和10:1之间,不过有时候这个值也可能很是很是大。
扫描的行数和访问类型
在评估查询开销的时候,须要考虑一下从表中找到某一行数据的成本。 MySQL有好儿 种访问方式能够查找井返回一行结果。 有些访问方式可能须要扫描不少行才能返回一行结果,也有些访问方式可能无须扫描就能返回结果。
在EXPLAIN语句中的type列反应了访问类型。 访问类型有不少种,从全表扫描到索引扫描、 范围扫描、 惟一索引查询、 常数引用等。 这里列的这些,速度是从慢到快,扫描的 行数也是从小到大。 你不须要记住这些访问类型,但须要明白扫描表、 扫描索引、 范围访问和单值访问的概念。
若是查询没有办法找到合适的访问类型,那么解决的最好办法一般就是增长一个合适的索引。
MySQL可以使用以下三种方式应用WHERE条件, 从好到坏依次为 :
MySQL不会告诉咱们生产结果实际上须要扫描多少行数据,而只会告诉咱们生产结果时一共扫描了多少行数据。
若是发现查询须要扫描大量的数据但只返回少数的行, 那么一般能够尝试下面的技巧去优化它:
设计查询的时候一个须要考虑的重要问题是, 是否须要将一个复杂的查询分红多个简单的查询。在传统实现中, 老是强调须要数据库层完成尽量多的工做, 这样作的逻辑在于之前老是认为网络通讯、 查询解析和优化是一件代价很高的事情。
可是这样的想法对于MySQL并不适用,MySQL从设计上让链接和断开链接都很轻量级,在返回一个小的查询结果方面很高效。 现代的网络速度比之前要快不少, 不管是带宽仍是延迟。 在某些版本的MySQL上, 即便在一个通用服务器上, 也可以运行每秒超过10万的查询, 即便是一个千兆网卡也能轻松知足每秒超过2000次的查询。 因此运行多个小查询如今已经不是大问题了。
MySQL内部每秒可以扫描内存中上百万行数据, 相比之下, MySQL响应数据给客户端就慢得多了。在其余条件都相同的时候,使用尽量少的查询固然是更好的。可是有时候,将一个大查询分解为多个小查询是颇有必要的。 别惧怕这样作, 好好衡量一下这样作是 不是会减小工做量。 稍后咱们将经过本章的一个示例来展现这个技巧的优点。
在应用设计的时候, 若是一个查询可以胜任时还写成多个独立查询是不明智的。
有时候对于一个大查询咱们须要 “分而治之”,将大查询切分红小查询, 每一个查询功能彻底同样, 只完成一小部分, 每次只返回一小部分查询结果。
按期的清楚大量数据时,若是用一个大的语句一次性完成的话,则可能须要一次性锁住不少数据、占满整个事物日志、耗尽系统资源、阻塞不少小的但很重要的查询。
不少高性能的应用都会对关联查询进行分解。例如:
SELECT * FROM tag JOIN tag_post ON tag_post.tag_id = tag.id JOIN post ON tag_post.post_id = post.id where tag.tag ='mysql';
能够分解成下面这些查询来代替:
SELECT * FROM tag WHERE tag='mysql'; SELECT * FROM tag_post where tag_id ='1234'; SELECT * FROM post WHERE post.id in(123,456,789,8989);
用分解关联查询的方式重构查询有以下的优点:
当但愿MySQL可以以更高的性能运行查询时,最好的办法就是弄清楚MySQL是如何一点,不少查询优化工做实际上就是遵循一些原则让优优化和执行查询的。一且理解这一点,不少查询优化工做实际上就是遵循一些原则让优化器可以按照预想的合理的方式运行。
MySQL客户端和服务器之间的通讯协议是 “半双工 ” 的,这意味着,在任何一个时刻,要么是由服务器向客户端发送数据,要么是由客户端向服务器发送数据,这两个动做不能同时发生。因此,咱们没法也无须将一个消息切成小块独立来发送。
这种协议让MySQL通讯简单快速,可是也从不少地方限制了MySQL。一个明显的限制是,这意味着无法进行流量控制。一且一端开始发生消息, 另外一端要接收完整个消息才能响应它。这就像来回抛球的游戏:在任什么时候刻,只有一我的能控制球,并且只有控制球的人才能将球抛回去(发送消息)。
多数链接MySQL的库函数均可以得到所有结果集并缓存到内存里,还能够逐行获取须要的数据。默认通常是得到所有结果集并缓存到内存中。MySQL一般须要等全部的数据都已经发送给客户端才能释放这条查询所占用的资源,因此接收所有结果并缓存一般能够减小服务器的压力,让查询可以早点结束、早点释放相应的资源。可是若是返回一个很大的结果集的时候,这样作并很差,由于库函数会花不少时间和内存来存储全部的结果集。这种状况下能够不使用缓存来记录结果而是直接处理。
查询状态
对于一 个MySQL 链接,或者说一个线程,任什么时候刻都有一个状态,该状态表示了MySQL当前正在作什么。有不少种方式能查看当前的状态,最简单的是使用SHOW FULL PROCESS LIST命令(该命令返回 结果中的Command 列就表示当前的状态)。在一 个查询的生命周期中,状态会变化不少次。MySQL官方手册中对这 些状态值的含义有最权威的解释,下面将这些状态列出来,并作一个简单的解释。
Sleep 线程正在等待客户端发送新的请求。
Query 线程正在执行查询 或者正在将结果发送给客户端。
Locked 在MySQL服务器层,该线程正在等待表 锁。在存储引擎级别实现的锁,例如 InnoDB的行锁,并不会体如今线程状态中。对千MyISAM来讲这是一 个比较典型的状态,但在其余没有行锁的引擎中也常常会出现。
Analyzing and statistics 线程正在收集存储引擎的统计信息,井生成查询的执行计划。
Copying to tmp table [on disk] 线程正在执行查询,而且将其结果集都复制到一个临时表中,这种状态通常要么是在作GROUP BY操做,要么是文件排序操做,或者是UNION操做。若是这个状态后面还有"on disk"标记,那表示MySQL正在将一个内存临时表放到磁盘上。
Sorting result 线程正在对结果集进行排序。
Sending data 这表示多种状况:线程可能在多个状态之间传送数据, 或者在生成结果集, 或者在向客户端返回数据。
在解析一个查询语句以前, 若是查询缓存是打开的, 那么MySQL会优先检查这个查询是否命中查询缓存中的数据。 这个检查是经过一个对大小写敏感的哈希查找实现的。 查询和缓存中的查询即便只有一个字节不一样, 那也不会匹配缓存结果注 11' 这种状况下查询就会进入下一阶段的处理。
若是当前的查询刚好命中了查询缓存, 那么在返回查询结果以前MySQL会检查一次用户权限。 这仍然是无须解析查询SQL语句的, 由于在查询缓存中已经存放了 当前查询须要访问的表信息。 若是权限没有问题, MySQL会跳过全部 其余阶段, 直接从缓存中拿到结果并返回给客户端。 这种状况下,查询不会被解析,不用生成执行计划,不会被执行。
查询的生命周期的下一步是将一个SQL转换成一个执行计划, MySQL再依照这个执行计划和存储引擎进行交互。 这包括多个子阶段:解析SQL、 预处理 、 优化SQL执行计划。这个过程当中任何错误(例如语法错误)均可能终止查询。 这里不打算详细介绍MySQL内部实现, 而只是选择性地介绍其中几个独立的部分, 在实际执行中, 这几部分可能一块儿执行也可能单独执行。 咱们的目的是帮助你们理解MySQL如何执行查询, 以便写出更优秀的查询。
语法解析器和预处理
首先, MySQL经过关键字将SQL语句进行解析, 并生成 一棵对应的 “ 解析树 ” 。 MySQL解析器将使用MySQL语法规则验证和解析查询。 例如, 它将验证是否使用错误的关键字,或者使用关键字的顺序是否正确等,再或者它还会验证引号是否能先后正确匹配。
预处理器则根据一些MySQL规则进一步检查解析树是否合法,例如,这里将检查数据表和数据列是否存在,还会解析名字和别名,看看它们是否有歧义。
下一步预处理器会验证权限。 这一般很快,除非服务器上有很是多的权限配置。
查询优化器
如今语法树被认为是合法的了,而且由优化器将其转化成执行计划。 一条查询能够有很 多种执行方式,最后都返回相同的结果。 优化器的做用就是找到这其中最好的执行计划。
MySQL使用基于成本的优化器,它将尝试预测一个查询使用某种执行计划时的成本,并选择其中成本最小的一个。 最初,成本的最小单位是随机读取一个4K数据页的成本,后来(成本计算公式)变得更加复杂,井且引入了一些 “因子” 来估算某些操做的代价, 如当执行一次WHERE条件比较的成本。 能够经过查询当前会话的Last_query_cost的值来得知MySQL计算的当前查询的成本。
SELECT SQL_NO_CACHE COUNT(*) FROM sakila.film_actor; SHOW STATUS LIKE 'Last_query_cost';
有不少种缘由会致使MySQL优化器选择错误的执行计划,以下所示:
MySQL的查询优化器是一个很是复杂的部件,它使用了不少优化策略来生成一个最优的执行计划。优化策略能够简单地分为两种,一种是静态优化,一种是动态优化。静态优化能够直接对解析树进行分析,井完成优化。例如,优化器能够经过一些简单的代数变换将WHERE条件转换成另外一种等价形式。静态优化不依赖于特别的数值,如WHERE条件中带入的一些常数等。静态优化在第一次完成后就一直有效,即便使用不一样的参数重复执行查询也不会发生变化。能够认为这是一种“ 编译时优化”。
相反,动态优化则和查询的上下文有关,也可能和不少其余因素有关,例如WHERE条件中的取值、索引中条目对应的数据行数等。这须要在每次查询的时候都从新评估,能够认为这是“运行时优化”。
在执行语句和存储过程的时候,动态优化和静态优化的区别很是重要。MySQL对查询的静态优化只须要作一次,但对查询的动态优化则在每次执行时都须要从新评估。有时候甚至在查询的执行过程当中也会从新优化。
下面是一些MySQL可以处理的优化类型:
从新定义关联表的顺序
数据表的关联井不老是按照在查询中指定的顺序进行。决定关联的顺序是优化器很重要的一部分功能,本章后面将深刻介绍这一点。
将外链接转化成内链接
并非全部的OUTER JOIN语句都必须之外链接的方式执行。诸多因素,例如WHERE条件、库表结构均可能会让外链接等价于一个内链接。MySQL可以识别这点并重写查询,让其能够调整关联顺序。
使用等价变换规则
MySQL可使用一些等价变换来简化并规范表达式。它能够合并和减小一些比较,还能够移除一些恒成立和一些恒不成立的判断。这些规则对于咱们编写条件语句颇有用,咱们将在本章后续继续讨论。
优化COUNT()、MIN()和MAX()
索引和列是否可为空一般能够帮助MySQL优化这类表达式。例如,要找到某一列的最小值,只须要查询对应B-Tree索引最左端的记录,MySQL能够直接获取索引的第一行记录。在优化器生成执行计划的时候就能够利用这一点,在B-Tree索引中,优化器会将这个表达式做为一个常数对待。相似的,若是要查找一个最大值,也只需读取B-Tree索引的最后一条记录。若是MySQL使用了这种类型的优化,那么在EXPLAIN中就能够看到"Select tables optimized away"。从字面意思能够看出,它表示优化器已经从执行计划中移除了该表,并以一个常数取而代之。相似的,没有任何WHERE条件的COUNT(*)查询一般也可使用存储引擎提供的一些优化(例如,MylSAM维护了一个变量来存放数据表的行数)。
预估并转化为常数表达式
当MySQL检测到一个表达式能够转化为常数的时候,就会一直把该表达式做为常数进行优化处理。例如,一个用户自定义变址在查询中没有发生变化时就能够转换为一个常数。数学表达式则是另外一种典型的例子。
在优化阶段,有时候甚至一个查询也可以转化为一个常数。一个例子是在索引列上执行MIN()函数。甚至是主键或者惟一键查找语句也能够转换为常数表达式。若是WHERE子句中使用了该类索引的常数条件,MySQL能够在查询开始阶段就先查找到这些值,这样优化器就可以知道井转换为常数表达式。
覆盖索引扫描
当索引中的列包含全部查询中须要使用的列的时候,MySQL就可使用索引返回须要的数据, 而无须查询对应的数据行, 在前面的章节中咱们已经讨论过这点了。
子查询优化
MySQL在某些状况下能够将子查询转换一种效率更高的形式, 从而减小多个查询屡次对数据进行访问。
提早终止查询
在发现已经知足查询需求的时候,MySQL老是可以马上终止查询。一个典型的例子就是当使用了LIMIT子句的时候。除此以外,MySQL还有几类状况也会提早终止查询, 例如发现了一个不成立的条件, 这时MySQL能够马上返回一个空结果。
等值传播
若是两个列的值经过等式关联,那么MySQL可以把其中一个列的WHERE条件传递到另外一列上。
列表IN()的比较
在不少数据库系统中,IN()彻底等同于多个OR条件的子句, 由于这二者是彻底等价的。MySQL中这点是不成立的,MySQL将 IN()列表中的数据先进行排序,而后经过二分查找的方式来肯定列表中的值是否知足条件,这是一个 O(log n)复杂度的操做,等价地转换成OR查询的复杂度为 O(n), 对千IN()列表中有大量取值的时候,MySQL的处理速度将会更快。
上面列举的远不是MySQL优化器的所有,MySQL还会作大量其余的优化,即便本章全 部用来描述也会篇幅不足,但上面的这些例子已经足以让你们明白优化器的复杂性和智能性了。
数据和索引的统计信息
MySQL如何执行关联查询
MySQL中“关联” 词所包含的意义比通常意义上理解的要更普遍。总的来讲,MySQL认为任何一个查询都是一次“关联”—— 并不只仅是一个查询须要到两个表匹配才叫关联,因此在MySQL中,每个查询,每个片断(包括子查询,甚至基于单表的SELECT) 均可能是关联。
对于UNION查询,MySQL先将一系列的单个查询结果放到一个临时表中,而后再从新读出临时表数据来完成UNION查询。在MySQL的概念中,每一个查询都是一次关联,因此读取结果临时表也是一次关联。
当前MySQL关联执行的策略很简单:MySQL对任何关联都执行嵌套循环关联操做,即MySQL先在一个表中循环取出单条数据,而后再嵌套循环到下一个表中寻找匹配的行,依次下去,直到找到全部表中匹配的行为止。而后根据各个表匹配的行,返回查询中须要的各个列。MySQL会尝试在最后一个关联表中找到全部匹配的行,若是最后一个关联表没法找到更多的行之后, MySQL返回到上一层次关联表, 看是否可以找到更多的匹配记录, 依此类推迭代执行。
按照这样的方式查找第一个表记录, 再嵌套查询下一个关联表, 而后回溯到上一个表,在MySQL中是经过嵌套循环的方式实现一正如其名 “嵌套循环关联”。
从本质上说,MySQL多全部的类型的查询都以一样的方式运行。例如,MySQL在FROM子句中遇到子查询时,先执行子查询并将其结果放到一个临时表中,而后将这个临时表看成一个普通表对待。MySQL在执行UNIONv查询时也使用相似的临时表,在遇到右外链接的时候,MySQL将其改写成等价的左外链接。简而言之,当前版本的MySQL会将全部的查询类型都转换成相似的执行计划。
执行计划
和不少其余关系数据库不一样,MySQL井不会生成查询字节码来执行查询。MySQL生成 查询的一棵指令树,而后经过存储引擎执行完成这棵指令树井返回结果。最终的执行计划包含了重构查询的所有信息。若是对某个查询执行EXPLAIN EXTENDED后,再执行SHOW WARNINGS, 就能够看到重构出的查询。
MySQL老是从一个表开始一直嵌套循环、回溯完成全部表关联。
关联查询优化器
MySQL优化器最重要的一部分就是关联查询优化,它决定了多个表关联时的顺序。一般多表关联的时候,能够有多种不一样的关联顺序来得到相同的执行结果。关联查询优化器则经过评估不一样顺序时的成原本选择一个代价最小的关联顺序。
排序优化
不管如何排序都是一个成本很高的操做,因此从性能角度考虑,应尽量避免排序或者尽量避免对大量数据进行排序。
若是须要排序的数据量小于 “排序缓冲区", MySQL使用内存进行 “快速排序” 操做。 若是内存不够排序,那么MySQL会先将数据分块, 对每一个独立的块使用 “快速排序” 进行排序,并将各个块的排序结果存放在磁盘上,而后将各个排好序的块进行合并最后返回排序结果。
两次传输排序(就版本使用)
读取行指针和须要排序的字段,对其进行排序,而后再根据排序结果读取所须要的数据行。 这须要进行两次数据传输,即须要从数据表中读取两次数据,第二次读取数据的时候,由于是读取排序列进行排序后的全部记录,这会产生大量的随机I/O,因此两次数据传输的成本很是高。
单次传输排序(新版本使用)
先读取查询所须要的全部列,而后再根据给定列进行排序,最后直接返回排序结果。这个算法只在MySQL 4.1和后续更新的版本才引入。 由于再也不须要从数据表中读取两次数据,对于I/O密集型的应用,这样作的效率高了不少。另外,相比两次传输排序,这个算法只须要一次顺序1/0 读取全部的数据,而无须任何的随机I/O。 缺点是,若是须要返回的列很是多、 很是大,会额外占用大量的空间,而这些列 对排序操做本 身来讲是没有任何做用的。 由于单条排序记录很大,因此可能会有更多的排序块须要合并。
在关联查询的时候若是须要排序,MySQL会分两种状况来处理这样的文件排序。若是ORDER BY子句中的全部列都来自关联的地一个表,那么MySQL在关联处理地一个表的时候就进行文件排序。除此以外的全部状况,MySQL都会先将关联的结果存放到一个临时表中,而后在全部的关联都结束后,在进行文件排序。
在解析和优化阶段,MySQL将生成查询对应的执行计划,MySQL的查询执行引擎则根据这个执行计划来完成整个查询。 这里执行计划是一个数据结构, 而不是和不少其余的关系型数据库那样会生成对应的字节码。
相对于查询优化阶段, 查询执行阶段不是那么复杂:MySQL只是简单地根据执行计划给出的指令逐步执行。在根据执行计划逐步执行的过程当中,有大量的操做须要经过调用存储引擎实现的接口来完成。
查询执行的最后一个阶段是将结果返回给客户端。 即便查询不须要返回结果集给客户端, MySQL仍然会返回这个查询的一些信息, 如该查询影响到的行数。
若是查询能够被缓存, 那么MySQL在这个阶段也会将结果存放到查询缓存中。
MySQL将结果集返回客户端是一个增量、 逐步返回的过程。
这样处理有两个好处:服务器端无须存储太多的结果,也就不会由于要返回太多结果而 消耗太多内存。另外,这样的处理也让MySQL客户端第一时间得到返回的结果。
结果集中的每一行都会以一个知足MySQL客户端/服务器通讯协议的封包发送, 再经过TCP协议进行传输, 在TCP传输的过程当中, 可能对MySQL的封包进行缓存而后批量传输。
MySQL的万能 ”嵌套循环” 并非对每种查询都是最优的。 不过还好, MySQL查询优化器只对少部分查询不适用, 并且咱们每每能够经过改写查询让MySQL高效地完成工做。
MySQL的子查询实现得很是糟糕。 最糟糕的一类查询是WHERE条件中包含IN()的子查询语句。使用IN()加子查询, 性能常常会很是糟, 因此 一般建议使用EXISTS()等效的改写查询来获取更好的效率。
SELECT * FROM sakila.film where film_id IN(SELECT film_id FROM sakila.film_actor WHERE actor_id = 1);
通常会认为MySQL会先执行子查询返回全部包含actor_id 为1的film_id。通常来讲,IN()列表查询速度很快,因此有人会认为上面的查询会这样执行:
MySQL会将相关的外层表压到子查询中,它认为这样能够更效率地差找到数据行。MySQL会将查询改写成这样子:
SELECT * FROM sakila.film WHERE EXISTS(SELECT * FROM sakila.film_actor WHERE actor_id = 1 AND film_actor.film_id = film.film_id);
若是外层的表是一个很是大的表,那么这个查询的性能会很是糟糕。可使用下面的方法重写查询:
SELECT film.* FROM sakila.film INNER JOIN sakila.film_actor USING (film_id) WHERE actor_id = 1;
另外一个优化的办法是使用函数GROUP_CONCAT()在IN()中构造一由逗号分隔的列表。
如何用好关联子查询
井不是全部关联子查询的性能都会不好。 若是有人跟你说:“别用关联子查询"' 那么不要理他。 先测试,而后作出本身的判断。 不少时候,关联子查询是一种很是合理、 天然, 甚至是性能最好的写法。
有时,MySQL没法将限制条件从外层 “下推” 到内层,这使得本来可以限制部分返回结果的条件没法应用到内层查询的优化上。
若是但愿UNION的各个子句可以根据LIMIT只取部分结果集,或者但愿可以先排好序再合并结果集的话,就须要在UNION的各个子句中分别使用这些子句。
在5.0和更新的版本中,当WHERE子句中包含多个复杂条件的 时候,MySQL可以访问单个表的多个索引以合井和交叉过滤的方式来定位须要查找的行。
某些时候,等值传递会带来一些意想不到的额外消耗。例如,有一个很是大的IN()列表,而MySQL优化器发现存在WHERE、ON或者USING的子句,将这个列表的值和另外一个表的某个列相关联。
那么优化器会将IN()列表都复制应用到关联的各个表中。一般,由于各个表新增了过滤条件,优化器能够更高效地从存储引擎过滤记录。可是若是这个列表很是大,则会致使优化和执行都会变慢。
MySQL没法利用多核特性来井行执行查询。 不少其余的关系型数据库可以提供这个特性,可是MySQL作不到。 这里特别指出是想告诉读者不要花时间去尝试寻找并行执行查询的方法。
MySQL并不支持哈希关联——MySQL的全部关联都是嵌套循环关联。能够经过创建一个哈希索引来曲线地实现哈希关联。
MySQL并不支持松散索引扫描,也就没法按照不连续的方式扫描一个索引。一般,MySQL的索引扫描须要先定义一个起点和终点,即便须要的数据只是这段索引中不多数几个,MySQL仍须要扫描这段索引中每个条目。
假设有以下索引(a,b),有下面的查询:
SELECT ... FROM tb1 WHERE b BETWEEN 2 AND 3;
由于索引的前导字段是列a,可是在查询中指定了字段b,MySQL没法使用索引,从而只能经过全表扫描找到匹配的行。
了解索引的物理结构的话,不难发现这还能够有一个更快的办法执行上面的查询。索引的物理结构(不是存储引擎的API)使得能够先扫描a列地一个值对应的b列的范围,而后在跳到a列第二个不一样值扫描对应的b列的范围。
使用松散索引扫描效率会更高,可是MySQL如今还不支持这么作。
MySQL5.0以后的版本,在某些特殊的场景下是可使用松散索引扫描的。
SELECT actor_id,MAX(film_id) FROM sakila.film_actor GROUP BY actor_id;
对于MIN()和MAX()查询,MySQL的优化作的并很差。例如:
SELECT MIN(actor_id) FROM sakila.actor WHERE first_name ='PENELOPE';
由于在first_name字段上并无索引,所以MySQL会进行一次全表扫描,若是MySQL可以进行主键扫描。一个曲线的优化办法是移除MIN(),而后使用LIMIT来将查询重写:
SELECT actor_id FROM sakila.actor USE INDEX(PRIMARY) WHERE first_name = 'PENELOPE' LIMIT 1;
有时候为了获取更高的性能,不得不放弃一些原则。
MySQL不容许对同一张表同时进行查询和更新。这其实并非优化器的限制,若是清楚MySQL是如何执行查询的,就能够避免这种状况。
UPDATE tb1 AS outer_tb1 SET cnt = (SELECT count(*) FROM tb1 AS inner_tb1 WHERE inner_tb1.type = outer_tb1.type));
下面的查询将会正常执行:
UPDATE tb1 INNER JOIN(SELECT type,count(*) AS cnt FROM tb1 GROUP BY type)) AS der USING(type) SET tb1.cnt = der.cnt;
若是对优化器选择的执行计划不满意,可使用优化器提供的几个提示来控制最终的执行计划。可使用的一些提示以下:
HIGH_PRIORITY和LOW_PRIORITY 这个提示告诉MySQL,当多个语句同时访问某一个表的时候,哪些语句的优先级相对高些、哪些语句的优先级相对低些。 HIGH_PRIORITY用于SELECT语句的时候,MySQL会将此SELECT语句从新调度到全部正在等待表锁以便修改数据的语句以前。实际上MySQL是将其放在表的队列的最前面,而不是按照常规顺序等待。 LOW_PRIORITY则正好相反:它会让该语句一直处于等待状态,只要队列中还有须要访问同一个表的语句——即便是那些比该语句还晚提交到服务器的语句。 这两个提示只对使用表锁的存储引擎有效,千万不要在InnoDB或者其余有细粒度锁机制和并发控制的引擎中使用。即便实在MyISAM中使用也要注意,由于这两个提示会致使并发插入被禁用,可能会严重下降性能。
DELAYED 这个提示对INSERT和REPLACE有效。MySQL会将使用该提示的语句当即返回给客户端,并将插入的行数据放入到缓冲区,而后在表空闲时批量将数据写入。日志系统使用这样的提示很是有效,或者是其余须要写入大量数据可是客户端却不须要等待单条语句完成I/O的应用。这个用法有一些限制:并非全部的存储引擎都支持这样的作法;而且该提示会致使函数LAST_INSERT_ID()失效。
STRAIGHT_JOIN 这个提示能够放置在SELECT语句的SELECT关键字以后,也能够放置在任何两个关联表的名字之间。第一个用法是让查询中全部的表按照在语句中出现的顺序进行关联。第二个用法则是固定其先后两个表的关联顺序。 当MySQL没能选择正确的关联顺序的时候,或者因为可能的顺序太多致使MySQL没法评估全部的关联顺序的时候,STRAIGHT_JOIN都会颇有用。在后面这种状况,MySQL可能会花费大量时间在‘statistics’状态,加上这个提示则会大大减小优化器的搜索空间。 能够先使用EXPLAIN语句来查看优化器选择的关联顺序,而后使用该提示来重写查询,在看看它的关联顺序。当你肯定不管怎样where条件,某个固定的关联顺序始终是最佳的时候,使用这个提示能够大大提升优化器的效率。
SQL_SMALL_RESULT和SQL_BIG_RESULT 这两个提示只对SELECT语句有效。他们告诉优化器对GROUP BY或者DISTINCT查询如何使用临时表及排序。SQL_SMALL_RESULT告诉优化器对GROUP BY或者DISTINCT查询如何使用临时表及排序。SQL_SMALL_RESULT告诉优化器结果集会很小,能够将结果集放到内存中的索引临时表,以免排序操做。若是是SQL_BIG_RESULT,则告诉优化器结果集可能会很是大,建议使用磁盘临时表作排序操做。
SQL_BUFFER_RESULT 这个提示告诉优化器将查询结果放入到一个临时表,而后尽量快地释放表锁。这和前面提到的有由客户端缓存结果不一样。当你无法使用客户端缓存的时候,使用服务器端的缓存一般颇有效。带来的好处是无须在客户端上消耗太多内存,还能够尽量快地释放对应的表锁。代价是,服务器端将须要更多的内存。
SQL_CACHE和SQL_NO_CACHE 严格来讲,这并非一个优化器提示。它不会告诉优化器任何关于执行计划的东西。他会让MySQL返回的结果集包含更多的信息。查询中加上该提示MySQL会计算除去LIMIT子句后这个查询要返回的结果集的总数,而实际上只返回LIMIT要求的结果集。
FOR UPDATE和LOCK IN SHARE MODE 这也不是真正的优化其提示。这两个提示主要控制SELECT语句的锁机制,但只对实现了行级锁的存储引擎有效。使用该提示会对符合查询条件的数据行加锁。对于INSERT... SELECT语句是不须要这两个提示的,由于对于MySQL5.0和更新版本会默认给这些记录加上读锁。 惟一内置的支持这两个提示的引擎就是InnoDB。另外须要记住的是,这两个提示会让某些优化没法正常使用,例如索引覆盖扫描。这两个提示常常被滥用,很容易形成服务器的锁争用问题。
USE INDEX、IGNORE INDEX和FORCE INDEX 这几个提示会告诉优化器使用或者不使用哪些索引来查询记录。在MySQL5.0和更早的版本,这些提示并不会影响到优化器选择哪一个索引进行排序和分组,在MySQSL5.1和以后的版本能够经过新增选项FOR ORDER BY和FOR GROUP BY来指定是否排序和分组有效。 FORCE INDEX和USE INDEX基本相同,除了一点:FORCE INDEX会告诉优化器全表扫描的成本会远远高于扫描索引,或者由于某些缘由要使用另外一个索引时,可使用该提示。
optimizer_search_depth 这个参数控制优化器在穷举执行计划的限度。若是查询长时间处于‘statistics’状态,那么能够考虑调低次参数。
optimizer_prune_level 该参数默认是打开的,这让优化器会根据须要扫描的行数来决定是否跳过某些执行计划。
optimizer_switch 这个变量包含了一些开启/关闭优化器特性的标志位。
COUNT()是一个特殊的函数,有两种很是不一样的做用:它能够统计某个列值的数量,也能够统计行数。 在统计列值时要求列值是非空的(不统计 NULL)。若是在 COUNT() 的括号中指定了列或者列的表达式,则统计的就是这个表达式有值的结果数。由于不少人对 NULL 理解有问题,因此这里很容易产生误解。若是想了解更多关于SQL语句中NULL的含义,建议阅读一些关千SQL语句基础的书籍。(关于这个话题,互联网上的一些信息是不够精确的。)
COUNT()的另外一个做用是统计结果集的行数。当MySQL确认括号内的表达式值不可能为空时,实际上就是在统计行数。最简单的就是当咱们使用 COUNT(*) 的时候,这种状况下通配符*井不会像咱们猜测的那样扩展成全部的列,实际上,它会忽略全部的列而直接统计全部的行数。
关于MyISAM的神话
MyISAM的COUNT()函数老是很是快,不过这是有前提条件的,即只有没有任何WHERE条件的COUNT()才很是快,所以此时无须实际地去计算表的行数。若是MySQL知道某列col不可能为NULL值,那么MySQL内部会将COUNT(col)表达式优化为COUNT()。当统计带WHERE子句的结果集行数,能够是统计某个列值的数量时,MyISAM的COUNT()和其余存储引擎没有任何不一样,就再也不有神话般的速度了。
简单优化
有时候可使用MyISAM在COUNT(*)全表很是快的这个个性,来加速一些特定条件的COUNT()的查询。
SELECT COUNT(*) FROM world.City WHERE ID > 5;
若是将条件反转一下
SELECT (SELECT COUNT(*) FROM world.City) - COUNT(*) FROM world.City WHERE ID <= 5;
使用近似值
有时候某些业务场景并不要求彻底精确的COUNT()值,此时能够用近似值来代替。EXPLAIN出来的优化器估算的行数就是一个不错的近似值,执行EXPLAIN并不须要真正地去执行查询,因此成本很低。
更复杂的优化
一般来讲,COUNT()都须要扫描大量的行(意味着要访问大量数据)才能得到精确的结果,所以是很难优化的。除了前面的方法,在MySQL层面还能作的就只有索引覆盖扫描了。
这个话题基本上整本书都在讨论, 这里须要特别提到的是:
关于子查询优化咱们给出的最重要的优化建议就是尽量使用关联查询代替,至少当前的MySQL版本须要这样。本章的前面章节已经详细介绍了这点。“尽量使用关联” 并非绝对的,若是使用的是MySQL5.6或更新的版本或者MariaDB,那么就能够直接忽略关于子查询的这些建议了。
在MySQL中,当没法使用索引的时候,GROUP BY使用两种策略来完成:使用临时表或者文件排序来作分组。
若是须要对关联查询作分组(GROUP BY),而且是按照查找表中的某个列进行分组,那么一般采用查找表的标识列分组的效率会比其余列更高。
优化此类分页查询的一个最简单的办法就是尽量地使用索引覆盖扫描,而不是查询全部的列。而后根据须要作一次关联操做再返回所需的列。对于偏移量很大的时候,这样作的效率会提高很是大。
SELECT film_id,description FROM sakila.film ORDER BY title LIMIT 50,5;
若是这个表很是大,那么这个查询最好该写成下面的样子:
SELECT film_id,description FROM sakila.film INNER JOIN(SELECT film_id FROM sakila.film ORDER BY title LIMIT 50,5) AS lim USING (film_id);
这里的延迟关联将大大提高查询效率,它让MySQL扫描尽量少的页面,获取须要访问的记录 后再根据关联列回原表查询须要的全部列。这个技术也能够用于优化关联查询中的LIMIT子句。
分页的时候,另外一个经常使用的技巧是在LIMIT语句中加上SQL_CALC_FOUND_ROWS提示,这样就能够得到去掉LIMIT之后知足条件的行数,所以能够做为分页的总数。无论是否须要,MySQL都会扫描全部知足条件的行,而后在抛弃掉不须要的行,而不是在知足LIMIT的行数后就终止扫描。 一个更好的设计是将具体的页数换成“下一页”按钮。 另外一种作法是先获取并缓存较多的数据——例如,缓存1000条——而后每次分页都从这个缓存中获取。
除非确实须要服务器消除重复的行,不然就必定要使用UNION ALL, 这一点很重要。 如 果没有ALL关键字,MySQL会给临时表加上 DISTINCT选项,这会致使对整个临时表的 数据作惟一性检查。 这样作的代价很是高。 即便有 ALL关键字, MySQL仍然会使用临时表存储结果。 事实上,MySQL老是将结果放入临时表,而后再读出,再返回给客户端。虽然不少时候这样作是没有必要的(例如,MySQL能够直接把这些 结果返回给客户端)。
Percona Toolkit中的qt-query-advisor可以解析查询日志、分析查询模式,而后给出全部可能存在潜在问题的查询,并给出足够详细的建议。
用户自定义变量是一个容易被遗忘的MySQL特性,可是若是可以用好,发挥其潜力,在某些场景能够写出很是高效的查询语句。在查询中混合使用过程化和关系化逻辑的时候,自定义变量可能会很是有用。单纯的关系查询将全部的东西都当成无序的数据集合,而且一次性操做它们。MySQL则采用了更加程序化的处理方式。MySQL的这种方式有它的弱点,但若是能熟练地掌握,则会发现其强大之处,而用户自定义变量也能够给这中种方式带来很大的帮助。
如下场景不能使用用户自定义变量:
优化排名语句
使用用户自定义变量的一个重要特性是你能够再给一个变量赋值的同时使用这个变量。
SET @rownum := 0; SELECT actor_id, @rownum := @rownum + 1 AS rownum FROM sakila.actor LIMIT 3;
返回每一个演员参演电影的数量:
SELECT actor_id, COUNT(*) as cnt FROM sakila.film_actor GROUP BY actor_id ORDER BY cnt DESC LIMIT 10;
如今咱们再把排名加上去,使用三个变量来实现:一个用来记录当前的排名,一个用来记录前一个演员的排名,还有一个用来记录当前演员参演的的电影数量。
SET @curr_cut := 0, @prev_cnt := 0, @rank := 0; SELECT actor_id, @curr_cnt := COUNT(*) AS cnt, @rank := IF(@prev_cnt <> @curr_cnt, @rank + 1, @rank ) AS rank, @prev_cnt := @curr_cnt AS dummy FROM sakila.film_actor GROUP BY actor_id ORDER BY cnt DESC LIMIT 10;
Oops——排名和统计列一直都没法更新,这是什么缘由? 这里经过EXPLAINA咱们看到将会使用临时表和文件排序,因此多是因为变量赋值的时间和咱们预料的不一样。
SET @curr_cut := 0, @prev_cnt := 0, @rank := 0; SELECT actor_id, @curr_cnt := cnt AS cnt, @rank := IF(@prev_cnt <> @curr_cnt, @rank + 1, @rank ) AS rank, @prev_cnt := @curr_cnt AS dummy FROM (SELECT actor_id,COUNT(*) AS cnt FROM sakila.film_actor GROUP BY actor_id ORDER BY cnt DESC LIMIT 10) as der;
避免重复查询刚刚更新的数据
UPDATE t1 SET lastUpdated = NOW() WHERE id = 1; SELECT lastUpdated FROM t1 WHERE id = 1;
使用变量,能够按以下方式重写查询:
UPDATE t1 SET lastUpdated = NOW() WHERE id = 1 AND @now := NOW(); SELECT @now;
统计更新和插入的数量
INSERT INTO t1(c1,c2) VALUES(4,4),(2,1),(3,1) ON DUPLICATE KEY UPDATE c1 = VALUES(c1) + (0 * (@x := @x + 1));
当每次因为冲突致使更新时对变量@x字增一次。而后经过对这个表达式乘以0来让其不影响要更新的内容。
肯定取值的顺序 使用用户自定义变量的一个最多见的问题就是没有注意到在赋值和读取变量的时候可能实在查询的不一样阶段。
SET @rownum := 0; SELECT actor_id, @rownum := @rownum + 1 AS cnt FROM sakila.actor WHERE @rownum<1;
由于WHERE和SELECT是在查询执行的不一样阶段被执行的。若是在查询中再加入ORDER BY的话,结果可能会更不一样:
SET @rownum := 0; SELECT actor_id, @rownum := @rownum + 1 AS cnt FROM sakila.actor WHERE @rownum<1 ORDER BY first_name;
解决这个问题的办法就是让变量的赋值和取值发生在执行查询的同一阶段:
SET @rownum := 0; SELECT actor_id, @rownum AS cnt FROM sakila.actor WHERE (@rownum := @rownum + 1) <=1;
将赋值语句放到LEAST()函数中,这样就能够在彻底不改变排序顺序的时候完成赋值操做。GREATEST()、LENGTH()、ISNULL()、NULLIFL()、IF()、COALESCE()。
SET @rownum := 0; SELECT actor_id, @rownum := @rownum + 1 AS cnt FROM sakila.actor WHERE @rownum<1 ORDER BY first_name,LEAST(0,@rownum := @rownum + 1);
编写偷懒的UNION
SELECT id FROM users WHERE id = 123 UNION ALL SELECT id FROM users_archived WHERE id = 123;
SELECT GREATEST(@found := -1, id) AS id, 'users' AS which_tb1 FROM users WHERE id = 1 UNION ALL SELECT id, 'users_archived' FROM users_archived WHERE id = 1 AND @found IS NULL UNION ALL SELECT 1, 'reset' FROM DUAL WHERE ( @found := NULL ) IS NOT NULL;
用户自定义变量的其余用处
不只实在SELECT语句中,在其余任何类型的SQL语句中均可以对变量进行赋值。 用户自定义变量可以作的有趣的事情:
若是把建立高性能应用程序比做是一个环环相扣的“难题”, 除了前面介绍的schema、索引和查询语句设计以外, 查询优化应该是解开“难题” 的最后一步了。要想写一个好的查询, 你必需要理解schema设计、索引设计等, 反之亦然。
理解查询是如何被执行的以及时间都消耗在哪些地方, 这依然是前面咱们介绍的响应时间的一部分。再加上一些诸如解析和优化过程的知识, 就能够更进一步地理解上一章讨论的MySQL如何访问表和索引的内容了。这也从另外一个维度帮助读者理解MySQL在访问表和索引时查询和索引的关系。
优化一般都须要三管齐下:不作、少作、快速地作。咱们但愿这里的案例可以帮助你将理论和实践联系起来。
除了这些基础的手段, 包括查询、表结构、索引等, MySQL还有一些高级的特性能够帮助你优化应用, 例如分区, 分区和索引有些相似可是原理不一样。MySQL还支持查询缓存,它能够帮你缓存查询结果,当彻底相同的查询再次执行时,直接使用缓存结果。