高性能MySQL(第三版)第六章:查询性能优化

第六章 查询性能优化

  • 6.1 为什么查询速度会慢
  • 6.2 慢查询基础:优化数据访问
    • 6.2.1 是否向数据库请求了不需要的数据
    • 6.2.2 MySQL是否在扫描额外的记录
  • 6.3 重构查询的方式
  • 6.4 查询执行的基础
    • 6.4.1 MySQL客户端、服务器端通信协议
    • 6.4.2 查询缓存
    • 6.4.3 查询优化处理
    • 6.4.4 查询执行引擎
    • 6.4.5 返回结果给客户端
  • 6.5 MySQL查询优化器的局限性
  • 6.6 查询优化器的提示
  • 6.7 优化特定类型的查询
  • 6.7.1 优化COUNT()查询
  • 6.7.2 优化关联查询
  • 6.7.3 优化子查询
  • 6.7.4 优化GROUP BY和DISTINCT
  • 6.7.5 优化LIMIT分页
  • 6.7.6 优化SQL_CALC_FOUND_ROWS
  • 6.7.7 优化UNION查询
  • 6.7.8 静态查询分析
  • 6.7.9 使用用户自定义变量
  • 6.8 总结

如何设计最优的库表结构、 如何建立最好的索引, 这些对于高性能来说是必不可少的。 但这些还不够一一还需要合理的设计查询。 如果查询写得很糟糕,即使库表结构再合理、 索引再合适, 也无法实现高性能。

6.1 为什么查询速度会慢

如果把查询看作是一个任务, 那么它由一系列子任务组成, 每个子任务都会消耗一定的时间。 如果要优化查询, 实际上要优化其子任务, 要么消除其中一些子任务, 要么减少子任务的执行次数,要么让子任务运行得更快。

查询的生命周期:从客户端,到服务器,然后在服务器上进行解析,生成执行计划,执行(包括调用存储引擎及调用后的排序、分组等数据处理),并返回结果给客户端。其中“执行”可以认为是整个生命周期中最重要的阶段,这其中包括了大量为了检索数据到存储引擎的调用以及调用后的数据处理,包括排序、分组等。

在完成这些任务时,查询需要在不同地方花费时间,包括网络,CPU 计算,生成统计信息和执行计划、锁等待(互斥等待)等操作,尤其是向底层存储引擎检索数据的调用操作,这些调用需要在内存操作、CPU操作和内存不足时导致的I/O操作上消耗时间。根据存储引擎不同,可能还会产生大量的上下文切换以及系统调用。

优化查询的目的就是减少和消除某些操作所花费的时间。

了解查询的生命周期、清楚查询的时间消耗情况对于优化查询有很大的意义。

6.2 慢查询基础:优化数据访问

查询性能低下最基本的原因是访问的数据太多。某些查询可能不可避免地需要筛选大量数据, 但这并不常见。大部分性能低下的查询都可以通过减少访问的数据量的方式进行优化。对于低效的查询, 我们发现通过下面两个步骤来分析总是很有效:

  • 确认应用程序是否在检索大量超过需要的数据。这通常意味着访问了太多的行或列
  • 确认MySQL服务器层是否在分析大量超过需要的数据行

6.2.1 是否向数据库请求了不需要的数据

有些查询会请求超过实际需要的数据, 然后这些多余的数据会被应用程序丢弃。这会给MySQL服务器带来额外的负担, 并增加网络开销。另外也会消耗应用服务器的CPU 和内存资源。 一些经典案例:

  • 查询不需要的记录
    一个常见的错误是常常会误以为MySQL会只返回需要的数据,实际上MySQL却是先返回全部结果集再进行计算。
  • 多表关联时返回全部列
    查询所有在电影Academy Dinosaur中出现的演员,千万不要按下面的写法编写查询:
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 ....;
  • 总是取出全部列
    每次看到SELECT *的时候都需要要用怀疑的眼光审视,是不是真的需要返回全部的列?很可能不是必须的。取出全部的列,会让优化器无法完成索引覆盖扫描这类优化,还会为服务器带来额外的I/O、内存和CPU消耗。 查询返回超过需要的数据也不是坏事。这种有点浪费数据库资源的方式可以简化开发,因为能提高相同代码片段的复用性,如果清楚这样做的性能影响,那么这种做法也是值得考虑的。获取并缓存所有的列的查询,相比多个独立的只获取部分列的查询可能有更有好处。
  • 重复查询相同的数据
    初次查询时将数据缓存,在需要时在缓存中取出来。

6.2.2 MySQL是否在扫描额外的记录

在确定查询只返回需要的数据以后,接下来应该看看查询为了返回结果是否扫描了过多 的数据。对于MySQL, 最简单的衡量查询开销的三个指标如下:

  • 响应时间
  • 扫描的行数
  • 返回的行数

没有哪个指标能够完美地衡量查询的开销,但它们大致反映了MySQL在内部执行查询时需要访问多少数据,并可以大概推算出查询运行的时间。 这三个指标都会记录到MySQL的慢日志中,所以检查慢日志记录是找出扫描行数过多的查询的好办法。

响应时间
响应时间是两个部分之和:服务时间和排队时间。 服务时间是指数据库处理这个查询真正花了多长时间。 排队时间是指服务器因为等待某些资源而没有真正执行查询的时间——可能是等I/O操作完成,也可能是等待行锁,等等。遗憾的是,我们无法把响应时间细分到上面这些部分,除非有什么办法能够逐个测量上面这些消耗,不过很难做到。 一般最常见和重要的等待是I/O和锁等待,但是实际情况更加复杂。

扫描的行数和返回的行数

分析查询时,查看该查询扫描的行数是非常有帮助的。 这在一定程度上能够说明该查询找到需要的数据的效率高不高。

对于找出那些 “糟糕” 的查询,这个指标可能还不够完美, 因为并不是所有的行的访问代价都是相同的。 较短的行的访问速度更快, 内存中的行也比磁盘中的行的访问速度要快得多

理想情况下扫描的行数和返回的行数应该是相同的。 但实际情况中这种 “美事” 并不多。例如在做一个关联查询时,服务器必须要扫描多行才能生成结果集中的一行。 扫描的行数对返回的行数的比率通常很小,一般在1:1和10:1之间,不过有时候这个值也可能非常非常大。

扫描的行数和访问类型

在评估查询开销的时候,需要考虑一下从表中找到某一行数据的成本。 MySQL有好几种访问方式可以查找并返回一行结果。 有些访问方式可能需要扫描很多行才能返回一行结果,也有些访问方式可能无须扫描就能返回结果。

在EXPLAIN语句中的type列反应了访问类型。 访问类型有很多种,从全表扫描到索引扫描、 范围扫描、 唯一索引查询、 常数引用等。 这里列的这些,速度是从慢到快,扫描的行数也是从小到大。 你不需要记住这些访问类型,但需要明白扫描表扫描索引范围访问单值访问的概念。

如果查询没有办法找到合适的访问类型,那么解决的最好办法通常就是增加一个合适的索引。

MySQL能够使用如下三种方式应用WHERE条件, 从好到坏依次为 :

  1. 在索引中使用WHERE条件来过滤不匹配的记录。 这是在存储引擎层完成的。
  2. 使用索引覆盖扫描(在Extra列中出现了Using index) 来返回记录, 直接从索引中过滤不需要的记录并返回命中的结果。 这是在MySQL服务器层完成的, 但无须再回表查询记录。
  3. 从数据表中返回数据,然后过滤不满足条件的记录(在Extra列中出现Using Where)。这在MySQL服务器层完成,MySQL需要先从数据表读出记录然后过滤。

MySQL不会告诉我们生产结果实际上需要扫描多少行数据,而只会告诉我们生产结果时一共扫描了多少行数据。

如果发现查询需要扫描大量的数据但只返回少数的行, 那么通常可以尝试下面的技巧去优化它

  • 使用索引覆盖扫描, 把所有需要用的列都放到索引中, 这样存储引擎无须回表获取对应行就可以返回结果了。
  • 改变库表结构。例如使用单独的汇总表。
  • 重写这个复杂的查询, 让MySQL优化器能够以更优化的方式执行这个查询。

6.3 重构查询的方式

  • 确定一个复杂查询还是多个简单查询更加有效
  • 切分查询
  • 分解关联查询

删除旧的数据是一个很好的例子。如果只用一条语句一次性执行一个大的删除操作,则可能需要一次锁住很多数据,占满整个事务日志,耗尽系统资源、阻塞很多小的但重要的查询。将一个大的删除操作分解成多个较小的删除操作可以将服务器上原本一次性的压力分散到多次操作上,尽可能小地影响MySql性能,减少删除时锁的等待时间。同时也减少了MySql主从复制的延迟。
另一个例子是分解关联查询,即对每个要关联的表进行单表查询,然后将结果在应用程序中进行关联。
将一个关联查询拆解成多个单表查询有如下优点
1.让缓存的效率更高。如果缓存的是关联查询的结果,那么其中的一个表发生变化,整个缓存就失效了。而拆分后,如果只是某个表很少的改动,并不会破坏所有的缓存。
2.可以减少锁的竞争
3.更容易对数据库进行拆分,更容易做到高性能和可扩展。
4.查询本身的效率也有可能会有所提升。例如上面用IN()代替关联查询比随机的关联更加高效。

  • 将一个完整的查询分散到多次小查询中(例如通过Limit)

6.4 查询执行的基础

MySQL执行查询的过程:
(1)客户端发送一条查询给服务器
(2)服务器先检查查询缓存,如果命中了缓存,则立即返回存储在缓存中的结果,否则进入下一个阶段
(3)服务器端进行SQL解析、预处理,再由优化器生成对应的执行计划
(4)将结果返回给客户端。
高性能MySQL(第三版)第六章:查询性能优化_第1张图片

6.4.1 MySQL客户端、服务器端通信协议

MySQL客户端和服务器端的通信协议是“半双工”的,在任何一个时刻,要么是服务器向客户端发送数据,要么是由客户端向服务器发送数据,这两个动作不能同时发生

当使用多数连接Mysql的库函数从Mysql中获取数据的时候,其结果看起来都像是从MySQL服务器获取数据,实际上都是从这个库函数的缓存中获取数据。多数情况下,这没有什么问题,但是如果需要返回一个很大的数据集的时候,这样做并不好,因为库函数会花费很多时间和内存来存储所有的结果集,如果能够尽早处理这些结果集,就能大大减少内存的消耗,这种情况下可以不适用缓存来处理记录结果而是直接处理,这样做的缺点是,对于服务器来说,需要查询完成之后才能释放资源,所以在和客户端交互的过程中,服务器的资源都是被这个查询所占用的。

查询状态
对于一个MySQL连接(一个线程),任何时刻都有一个状态,该状态表示了MySQL当前正在做什么。可以用Show (full) processlist查询。
Sleep : 线程正在等待客户端发送新的请求
Query: 线程正在执行查询或者将结果发送给客户端
Locked: 在MySQL服务器层,该线程正在等待表锁,在存储引擎级别实现的锁,例如InnoDB的行锁,并不会体现在线程状态中。
Analyzing and statistics:线程正在收集存储引擎的统计信息,井生成查询的执行计划。
Copying to tmp table [on disk]:线程正在执行查询,并且将其结果集都复制到一个临时表中,这种状态一般要么是在做GROUP BY操作,要么是文件排序操作,或者是UNION操作。如果这个状态后面还有"on disk"标记,那表示MySQL正在将一个内存临时表放到磁盘上。
Sorting result:线程正在对结果集进行排序。
Sending data:这表示多种情况:线程可能在多个状态之间传送数据, 或者在生成结果集, 或者在向客户端返回数据。

6.4.2 查询缓存

在解析一个查询语句之前,如果查询缓存是打开的,那么MySQL会优先检查这个查询是否命中这个查询缓存中的数据,这个检查是通过一个对大小写敏感的哈希查找实现的,查询和查询缓存中即使只有一个字节不同,也不会匹配缓存结果。
如果当前的查询恰好命中了查询缓存, 那么在返回查询结果之前MySQL会检查一次用户权限。 这仍然是无须解析查询SQL语句的, 因为在查询缓存中已经存放了当前查询需要访问的表信息。 如果权限没有问题, MySQL会跳过所有其他阶段, 直接从缓存中拿到结果并返回给客户端。 这种情况下,查询不会被解析,不用生成执行计划,不会被执行。

6.4.3 查询优化处理

语法解析器和预处理
MySQL通过关键字将SQL语句进行解析,并生成一颗对应的“解析树”,MySQL解析器将使用MySQL语法规则进行验证和解析查询(语法分析),预处理器则会根据一些MySQL规则进一步检查解析树是否合法(语义分析),之后会验证权限。

查询优化器

优化器将查询转化为执行计划,优化器的作用是找到最好的执行计划。

MySQL使用基于成本的优化器,它尝试预测一个查询使用某种执行计划的成本,并选择其中成本最小的一个。通过通过查询当前会话的last_query_cost的值来得知MySQL计算的当前查询的成本。优化器在评估成本的时候并不考虑任何层面的缓存,它假设读取任何数据都需要一次磁盘I/O

MySQL可以处理的优化类型:

(1) 重新定义关联表的顺序

(2) 将外连接转化为内连接

(3) 使用等价变化规则

(4) 优化COUNT(), MIN()和MAX() :例如要查找一个最小值,可以查询B-Tree索引的最左端的记录,如果要查询一个最大值,也只需要获取B-Tree索引的最后一条记录。

(5) 预估并转化为常数表达式

(6) 覆盖索引扫描:当索引中的列包含了所有查询中使用的列时,MySQL可以使用覆盖索引返回需要的数据,而无需查询对应的数据行。

(7) 子查询优化。

(8) 提前终止查询:当发现已经满足查询需求的时候,MySQL总是能够立刻终止查询,一个典型的例子就是当使用LIMIT子句的时候

(9) 等值传播:如果两个列通过等式关联,那么MySQL能够把其中一个列的WHERE条件传递到另外一个列上。

(10) 列表IN的优化。在很多数据库系统中,IN()完全等价于多个OR条件的子句,因为这两者是完全等价的。在MySQL中,会对IN列表中的数据进行排序,然后通过二分查找的方式确定列表中的值是否满足条件,对于IN列表中有大量取值的时候,MySQL的处理速度将会更快。

MySQL中如何执行关联查询

当前MySQL关联执行的策略很简单:对任何关联都执行嵌套循环关联操作,现在一个表中循环取出单条数据,然后再嵌套到下一个表中寻找匹配的行,如此下去,直到找到所有表中匹配的行为止,然后根据各个表中匹配的行,返回查询中需要的各个列。

执行计划
对某个查询执行EXPLAIN EXTENDED后,再执行SHOW WARNINGS,就可以看到重构出的查询。

关联优化查询器
决定最佳的表连接的顺序。可以用SELECT STRAIGHT_JOIN强制按照查询的顺序进行表关联。

排序优化
无论如何,排序都是一个成本很高的操作,所以从性能角度考虑,应该尽量避免排序或者尽可能避免对大量数据进行排序。当不能使用索引生成排序结果的时候,MySQL需要进行排序,如果数据小则在内存中排序,如果数据量大则需要使用磁盘排序,MySQL将这个过程统一称为文件排序。

MySQL使用两种排序算法:旧版本使用“二次传输排序”,新版本使用“单次传输排序”

(1)两次传输排序:

读取行指针和需要排序的字段,对其进行排序,然后根据排序结果去读取所需要的数据行。这需要两次数据传输,第二次读取的时候,因为是读取的排序后的所有记录,这会产生大量的随机I/O,所以两次数据传输的成本非常高。不过这样做的优点是:排序的时候尽量存储较少的数据,可以再内存中容纳尽量多的行数进行排序

(2)单次传输排序:

先读取需要的所有列,然后根据给定列进行排序,最后直接返回排序结果,因为不需要从数据表中读取两次数据,对于I/O密集型的应用,这样的效率高了不少。相比两次数据传输排序,这个算法只需要一次顺序I/O读取所有的数据,而无需任何的随机I/O

6.4.4 查询执行引擎

在解析和优化阶段,MySQL将生成查询对应的执行计划,MySQL的查询执行引擎则根据这个执行计划来完成整个查询。 这里执行计划是一个数据结构, 而不是和很多其他的关系型数据库那样会生成对应的字节码。

相对于查询优化阶段, 查询执行阶段不是那么复杂:MySQL只是简单地根据执行计划给出的指令逐步执行。在根据执行计划逐步执行的过程中,有大量的操作需要通过调用存储引擎实现的接口来完成。

6.4.5 返回结果给客户端

查询执行的最后一个阶段是将结果返回给客户端。 即使查询不需要返回结果集给客户端, MySQL仍然会返回这个查询的一些信息, 如该查询影响到的行数。

如果查询可以被缓存, 那么MySQL在这个阶段也会将结果存放到查询缓存中。

MySQL将结果集返回客户端是一个增量、 逐步返回的过程。

这样处理有两个好处:服务器端无须存储太多的结果,也就不会因为要返回太多结果而 消耗太多内存。另外,这样的处理也让MySQL客户端第一时间获得返回的结果。

结果集中的每一行都会以一个满足MySQL客户端/服务器通信协议的封包发送, 再通过TCP协议进行传输, 在TCP传输的过程中, 可能对MySQL的封包进行缓存然后批量传输。

6.5 MySQL查询优化器的局限性

关联子查询
MySQL的子查询实现得非常糟糕。 最糟糕的一类查询是WHERE条件中包含IN()的子查询语句。使用IN()加子查询, 性能经常会非常糟, 所以 通常建议使用EXISTS()等效的改写查询来获取更好的效率

如何用好关联子查询
并不是所有关联子查询的性能都会很差。 如果有人跟你说:“别用关联子查询"’ 那么不要理他。 先测试,然后做出自己的判断。 很多时候,关联子查询是一种非常合理、 自然, 甚至是性能最好的写法。

UNION的限制
有时,MySQL无法将限制条件从外层 “下推” 到内层,这使得原本能够限制部分返回结果的条件无法应用到内层查询的优化上。

如果希望UNION的各个子句能够根据LIMIT只取部分结果集,或者希望能够先排好序再合并结果集的话,就需要在UNION的各个子句中分别使用这些子句。

索引合并优化
在5.0和更新的版本中,当WHERE子句中包含多个复杂条件的 时候,MySQL能够访问单个表的多个索引以合并和交叉过滤的方式来定位需要查找的行。

等值传递
某些时候,等值传递会带来一些意想不到的额外消耗。例如,有一个非常大的IN()列表,而MySQL优化器发现存在WHERE、ON或者USING的子句,将这个列表的值和另一个表的某个列相关联。

那么优化器会将IN()列表都复制应用到关联的各个表中。通常,因为各个表新增了过滤条件,优化器可以更高效地从存储引擎过滤记录。但是如果这个列表非常大,则会导致优化和执行都会变慢。

并行执行
MySQL无法利用多核特性来并行执行查询。 很多其他的关系型数据库能够提供这个特性,但是MySQL做不到。 这里特别指出是想告诉读者不要花时间去尝试寻找并行执行查询的方法。

哈希关联
MySQL并不支持哈希关联——MySQL的所有关联都是嵌套循环关联。可以通过建立一个哈希索引来曲线地实现哈希关联。

松散扫描索引
MySQL并不支持松散索引扫描,也就无法按照不连续的方式扫描一个索引。通常,MySQL的索引扫描需要先定义一个起点和终点,即使需要的数据只是这段索引中很少数几个,MySQL仍需要扫描这段索引中每一个条目。

最大值和最小值优化
对于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;

6.6 查询优化器的提示

如果对优化器选择的执行计划不满意,可以使用优化器提供的几个提示来控制最终的执行计划。可以使用的一些提示如下:

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 这个变量包含了一些开启/关闭优化器特性的标志位。

6.7 优化特定类型的查询

6.7.1 优化COUNT()查询

COUNT()的作用
COUNT()是一个特殊的函数,有两种非常不同的作用:它可以统计某个列值的数量,也可以统计行数。 在统计列值时要求列值是非空的(不统计 NULL)。如果在 COUNT() 的括号中指定了列或者列的表达式,则统计的就是这个表达式有值的结果数。

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层面还能做的就只有索引覆盖扫描了。

6.7.2 优化关联查询

这个话题基本上整本书都在讨论, 这里需要特别提到的是:

  • 确保 ON 或者USING子句中的列上有索引。在创建索引的时候就要考虑到关联的顺序。当表A和表B用列c关联的时候, 如果优化器的关联顺序是B、A, 那么就不需要在 B表的对应列上建上索引。 没有用到的索引只会带来额外的负担。 一般来说, 除非 有其他理由, 否则只需要在关联顺序中的第二个表的相应列上创建索引。
  • 确保任何的GROUP BY和ORDER BY中的表达式只涉及到一个表中的列, 这样MySQL才有可能使用索引来优化这个过程。
  • 当升级MySQL的时候需要注意:关联语法 、 运算符优先级等其他可能会发生变化 的地方。 因为以前是普通关联的地方可能会变成笛卡儿积, 不同类型的关联可能会 生成不同的结果等。

6.7.3 优化子查询

关于子查询优化我们给出的最重要的优化建议就是尽可能使用关联查询代替,至少当前的MySQL版本需要这样。本章的前面章节已经详细介绍了这点。“尽可能使用关联” 并不是绝对的,如果使用的是MySQL5.6或更新的版本或者MariaDB,那么就可以直接忽略关于子查询的这些建议了。

6.7.4 优化GROUP BY和DISTINCT

在MySQL中,当无法使用索引的时候,GROUP BY使用两种策略来完成:使用临时表或者文件排序来做分组。

如果需要对关联查询做分组(GROUP BY),并且是按照查找表中的某个列进行分组,那么通常采用查找表的标识列分组的效率会比其他列更高。

6.7.5 优化LIMIT分页

优化此类分页查询的一个最简单的办法就是尽可能地使用索引覆盖扫描,而不是查询所有的列。然后根据需要做一次关联操作再返回所需的列。对于偏移量很大的时候,这样做的效率会提升非常大。

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子句。

6.7.6 优化SQL_CALC_FOUND_ROWS

分页的时候,另一个常用的技巧是在LIMIT语句中加上SQL_CALC_FOUND_ROWS提示,这样就可以获得去掉LIMIT以后满足条件的行数,因此可以作为分页的总数。不管是否需要,MySQL都会扫描所有满足条件的行,然后在抛弃掉不需要的行,而不是在满足LIMIT的行数后就终止扫描。 一个更好的设计是将具体的页数换成“下一页”按钮。 另一种做法是先获取并缓存较多的数据——例如,缓存1000条——然后每次分页都从这个缓存中获取。

6.7.7 优化UNION查询

除非确实需要服务器消除重复的行,否则就一定要使用UNION ALL, 这一点很重要。 如 果没有ALL关键字,MySQL会给临时表加上 DISTINCT选项,这会导致对整个临时表的数据做唯一性检查。 这样做的代价非常高。 即使有 ALL关键字, MySQL仍然会使用临时表存储结果。 事实上,MySQL总是将结果放入临时表,然后再读出,再返回给客户端。虽然很多时候这样做是没有必要的(例如,MySQL可以直接把这些结果返回给客户端)。

6.7.8 静态查询分析

Percona Toolkit中的qt-query-advisor能够解析查询日志、分析查询模式,然后给出所有可能存在潜在问题的查询,并给出足够详细的建议。

6.7.9 使用用户自定义变量

用户自定义变量是一个容易被遗忘的MySQL特性,但是如果能够用好,发挥其潜力,在某些场景可以写出非常高效的查询语句。在查询中混合使用过程化和关系化逻辑的时候,自定义变量可能会非常有用。单纯的关系查询将所有的东西都当成无序的数据集合,并且一次性操作它们。MySQL则采用了更加程序化的处理方式。MySQL的这种方式有它的弱点,但如果能熟练地掌握,则会发现其强大之处,而用户自定义变量也可以给这中种方式带来很大的帮助。

以下场景不能使用用户自定义变量:

  • 使用自定义变量的查询,无法使用查询缓存。
  • 不能在使用变量或者标识符的地方使用自定义变量,例如表名、列名和LIMIT子句中。
  • 用户自定义变量的生命周期是在一个连接中有效,所以不能用它们来做连接间的通信。
  • 如果使用连接池或者持久化连接,自定义变量可能让看起来毫无关系的代码发生交互。
  • 在5.0之前的版本,是大小写敏感的,所以要注意代码在不同MySQL版本间的兼容性问题。
  • 不能显式地声明自定义变量的类型。确定未定义变量的具体类型的时机在不同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语句中都可以对变量进行赋值。 用户自定义变量能够做的有趣的事情:

查询运行时计算总数和平均值。
模拟GROUP()语句中的函数FIRST()和LAST()。
对大量数据做一些数据计算。
计算一个大表的MD5散列值。
编写一个样本处理函数,当样本中的数值超过某个边界的时候将其变成0。
模拟读/写游标。
在SHOW语句的WHERE子句中加入变量值。

6.8 总结

如果把创建高性能应用程序比作是一个环环相扣的“难题”, 除了前面介绍的schema、索引和查询语句设计之外, 查询优化应该是解开“难题” 的最后一步了。要想写一个好的查询, 你必须要理解schema设计、索引设计等, 反之亦然。

理解查询是如何被执行的以及时间都消耗在哪些地方, 这依然是前面我们介绍的响应时间的一部分。再加上一些诸如解析和优化过程的知识, 就可以更进一步地理解上一章讨论的MySQL如何访问表和索引的内容了。这也从另一个维度帮助读者理解MySQL在访问表和索引时查询和索引的关系。

优化通常都需要三管齐下:不做、少做、快速地做。我们希望这里的案例能够帮助你将理论和实践联系起来。

除了这些基础的手段, 包括查询、表结构、索引等, MySQL还有一些高级的特性可以帮助你优化应用, 例如分区, 分区和索引有些类似但是原理不同。MySQL还支持查询缓存,它可以帮你缓存查询结果,当完全相同的查询再次执行时,直接使用缓存结果。

你可能感兴趣的:(数据库)