第六章 查询性能优化

1. 为什么查询速度会慢

如果把查询看作是一个任务,那么它由一系列子任务组成,每个子任务都会消耗一定的时间。如果要优化,无非是减少子任务数量,或者减少子任务的执行次数。
查询声明周期:生成计划,执行,返回结果给客户端。

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

1.确认应用程序是否早检索大量超过需要的数据。通常意味着访问了太多的行,但是有时候也可能是访问了太多的行,有时候也是可能访问了太多的列

  1. 确认MySQL服务器是否在分析大量超过需要的数据行

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

查询请求多余的数据,然后多余的数据会被应用程序丢弃。这会给MySQL服务器带来额外的负担。典型有下面四种情况:
1. 查询了不需要的记录
2. 多表关联时返回全部列
3. 总是提取全部列
4. 重复查询相同的数据

2. MySQL是否扫描了额外的记录

响应时间
响应时间时两个部分之和:服务时间和排队时间。服务时间指数据库处理这个查询真正花了多长时间。排队时间是指服务器等待资源而没有真正执行查询的时间。
扫描的行数和返回的行数
分析查询的时候,查看扫描的行数是非常有帮助的。理想的情况下扫描的行数和返回的行数是相同的,但是这种情况并不多
扫描的行数和访问类型
在评估查询开销的时候,需要考虑一下从表中找到某一行数据的成本。MySQL有好几种方式可以查找并返回一行结果。有些访问方式可以无须扫描就能返回查询结果。
EXPLAIN语句中的type反应了访问的类型。一般有全表扫描、范围扫描、唯一索引扫描、常数引用等。
一般MySQL能使用如下三种方式来应用Where条件:

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

如果发现查询需要扫描大量的数据,但只返回少量的行。可以通过如下的技巧去优化它:

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

3. 重构查询的方式

3.1. 一个复杂的查询还是多个简单查询

设计查询的时候考虑将复杂的查询分成多个简单的查询,传统的实现中,总是强调数据库完成尽可能多的工作。因为以前总认为网络通信查询解析和优化是一件代价很高的事情。这样的想法对于MySQL并不适用,MySQL设计上让断开和连接都很轻量级。返回小查询结果方面很高效。
MySQL内部每秒钟能扫描内存中上百万条数据,相比之下,MySQL响应给客户端就慢很多。其他条件相同的时候,使用尽量少的查询当然是更好的。但有时将大的查询分成小的是有必要的。

3.2. 切分查询

有时将大的查询分而治之。删除旧的数据就是很好的例子,定期删除大的数据的,如果用一个大的数据一次性完成的话,则可能要锁住很多数据,占满整个事务日志,耗费资源,阻塞很多小的但重要的查询。将一个大的查询分成多个小的查询可以尽可能的减小影响MySQL的性能,而且减小延迟,例如:

mysql> DELETE  FROM msessages WHERE cteated < DATE_SUB(NOW(),INTERVAL 3 MONTH)

可以分解为

rows_affect = 0
do {
    rows_affected = do_query(
          "DELETE FROM messages WHERE created < DATE_SUB(NOW, INTERVAL 3 MONTH
           LIMIT 10000")
} while rows_affected > 0

一次删除一万行数据,一般来说是一个比较小而且对服务器的影响也是最小的。(事务引擎,很多小事务能够更高效)
如果每次处理后,都暂停一会再做下一次删除,那么而可以降低服务器的影响,大大减少删除时锁的持有时间

3.3. 分解关联查询

很多高性能的应用都会对关联查询进行分解(将JOIN拆分成SELECT),

mysql> SELECT * FROM a JOIN b on a.id = b.id 
       JOIN c on b.idd = c.id WHERE a.id = 1;

改为如下查询

mysql> SELECT * FROM a WHRE a.id = 1;
mysql> SELECT * FROM b WHERE idd = 1234;
mysql> SELECT * FROM c WHRER id in (1, 2, 3, 5)

优势:

  • 让缓存效率更高。对MySQL查询缓存来说,如果关联中的某个表发生了变化,那么就无法查询缓存了,拆分后,如果a.id被缓存,就会跳过第一个查询。如果某个表很少改变,那么基于该表的查询据可以重复利用查询缓存结果
  • 将查询分解后,执行单个查询可以减少锁的竞争
  • 在应用层做管理,可以更容易对数据库进行拆分,更容易做到高性能和可扩展
  • 查询效率本身也会上升,在本例中,让IN替代关联查询,可以让MySQL按照ID顺序进行查询,这可能比随机的关联要高效
  • 减少冗余记录的查询。在应用层做关联查询,意味着对于某条记录只需要查询一次,而在数据库中做关联查询,则可能需要重复的访问一部分数据,从这点看,这样的重构还能减少网络和内存的消耗
  • 更进一步,相当于在应用中实现了哈希关联,而不是用MySQL嵌套循环关联。某些场景哈希关联的效率要高很多

4. 查询执行的基础

查询执行路径
  1. 客户端发送一条查询给服务器。
  2. 服务器先检查查询缓存,如果命中了缓存,则立刻返回查询中的结果
  3. 服务器进行SQL解析、预处理,再优化器生成对应的执行计划
  4. MySQL根据优化器生成的执行计划,调用存储引擎的API来执行查询
  5. 将结果返回给客户端

4.1. MySQL客户端/服务器通信协议

MySQL客户端和服务器之前的通信协议是"半双工"的,这意味着,在任意一个时刻,要么是由客户端向服务端发送数据,要么是服务端向客户端发送数据。所以, 我们无需将一个消息切成小块独立来发送。
这种方式有明显的限制,没法进行流量限制。一旦一端开始发生消息,另外一端要接受完整才能响应它。
客户端用一个单独的数据包将查询传给服务器。这样是为什么当查询语句很长的时候,参数是max_allowed_packed就显得特别重要。一旦客户端发送了请求,它能做的事情就只能是等待结果了
相反,一般服务器响应给用户的数据通常很多,由多个数据包组成。当服务器开始响应的是,客户端必须完整地接收整个返回结果
换一种方式解释:当客户端从服务器取数据的时候,看起来是一个拉数据的过程。但实际是MySQL在向客户端推送数据的过程。客户端不断地从接收从服务器推送的数据,客户端无法让服务器停下来
查询状态
对于一个MySQL连接,一般来说由一个状态组成。很多种方式能查看当前态,简单的是使用SHOW FULL PROCESSLIST命令;在一个查询的生命周期中,状态会变化很多次。
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
这表示多种情况:线程可能在多个状态键传送数据,或者在生成结果集,或者向客户端返回数据

4.2. 查询缓存

在解析一个查询语句之前,如果查询缓存是打开的,那么MySQL会优先检查这个查询是否命中查询缓存中的数据,这个查询是通过一个对大小写敏感的哈希查找实现,查询和缓存中的查询即使有一节不同,那也不会匹配缓存结果。这种情况下查询就会进入下一个阶段的处理

4.3. 查询优化处理

查询的生命周期的下一步是将一个SQL转换成一个执行计划,MySQL再依照这个执行计划和存储引擎进行交互。包括:解析SQL、预处理、优化SQL执行计划。这个过程中任何错误都可能终止查询。
语法解析器和预处理
首先,MySQL通过关键字将SQL语句进行解析,并生成对应的解析树。解析器使用MySQL语法规则验证和解析查询,例如,验证是否使用错误的关键字,或者使用关键字的顺序是否正确。
预处理则根据一些MySQL规则进一步检查解析树是否合法。例如,检查数据表和数据列是否存在,还会解析名字和别名,看看它们是否有歧义
查询优化器
现在语法器被认为是合法了,并且由优化器将其转化为执行计划。一条查询可以由很多执行方式,最后都返回的相同的结果。优化器的作用就是找到其中最好的执行计划
MySQL使用基于成本的优化器,它将尝试预测一个查询使用某种执行计划时的成本,并选择其中成本最小的一个。
很多中原因会导致MySQL优化器选择错误的执行计划:

  • 统计信息不准确。MySQL依赖存储引擎提供的统计信息来评估成本,但是有的存储引擎提供的信息是准确的,有的偏差可能非常大。例如,InnoDB因为其MVCC的架构,并不能维护一个数据表的行数的精确统计信息
  • 执行计划中的成本估算不等同于实际的执行的成本。所以即使统计信息精准,优化器给出的执行计划也可能不是最优的
  • MySQL的最优解基于成本模型选择最优的计划有时候并不是最快的执行方式。
  • MySQL从不考虑其他并发的执行查询,这可能会影响到当前查询速度
  • MySQL也并不是任何时候都基于成本的优化。有时会也会基于一些固定的规则。
  • MySQL不会考虑不受其控制的操作成本,例如执行存储过程或这用户自定义函数的成本
  • 优化器还可能无法估算所有可能的执行技术,所以可能错过实际上最优的执行计划

MySQL查询优化器是一个非常复杂的部件,它使用了许多优化策略来生成一个最优的执行计划。优化策略可以分为两种:静态优化,动态优化。MySQL对静态优化只会做一次,但对查询的动态优化则在每次执行的时候都需要重新评估。有时候甚至在查询的执行过程中也会重新优化
下面是一些MySQL能过处理的优化类型:
重新定义关联表的顺序
数据的关联并不总是按照在查询中指定的顺序进行。决定关联的顺序是优化器很重要的一部分功能。
将外连接转化成内连接
使用等价变换规则
优化COUNT()、MIN()和MAX()
预估并转化为常熟数表达式
覆盖索引g扫描
子查询优化
提前终止查询
等值传播
列表 IN()的比较

4.4. 查询执行引擎

4.5. 返回结果给客户端

5. MySQL优化查询器的局限性

5.1. 关联子查询

5.2. UNION的限制

5.3. 索引合并优化

5.4. 等值传递

5.5. 并行执行

5.6. 哈希关联

5.7. 索引松散扫描

5.8. 最大值和最小值优化

6. 查询优化器的提示

7. 优化特定类型的查询

7.1. 优化COUNT()查询

7.2. 优化关联查询

7.3. 优化子查询

7.4. 优化GROUP BY 和 DISTINCT

7.5. 优化LIMIT分页

7.6. 优化SQL_CALC_FOUND_ROWS

7.7. 优化UNION查询

7.8. 静态查询分析

7.9. 使用用户自定义变量

8. 案列学习

你可能感兴趣的:(第六章 查询性能优化)