《高性能Mysql》四-查询性能优化

一、为什么查询速度会慢

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

二、优化数据访问

对于低效的查询,我们发现通过下面两个步骤来分析总是很有效:
确认应用程序是否在检索大量超过需要的数据。这通常意味着访问了太多的行,但有时候也可能是访问了太多的列。
确认Mysql服务器层是否在分析大量超过需要的数据行。
(1)是否向数据库请求了不需要的数据

  1. 查询不需要的记录
  2. 多表关联时返回全部列
  3. 总是取出全部列
  4. 重复查询相同的数据
    (2)mysql是否在扫描额外的记录
    对于mysql,最简单的衡量查询开销的三个指标如下:
  5. 响应时间
  6. 扫描的行数
  7. 返回的行数
    响应时间
    响应时间是两个部分之和:服务时间和排队时间,服务时间是指数据库处理这个查询真正花了多长时间。排队时间是指服务器因为等待某些资源而没有真正执行查询的时间(可能是等待I/O操作完成,也可能是等待行锁等)。

扫描的行数和返回的行数

一般Mysql能够使用如下三种方式应用where条件,从好到坏依次为:

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

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

  1. 使用索引覆盖扫描,把所有需要用的列都放到索引中,这样存储引擎无须回表获得对应行就可以返回结果了。
  2. 改变库表结构
  3. 重写这个复杂的查询

三、重构查询的方式

(1)一个复杂查询还是多个简单查询
(2) 切分查询

有时候对于一个大查询我们需要“分而治之”, 将大查询切分成小查询,每个查询功能一样,只完成一小部分,每次只返回一小部分查询结果。
删除旧的数据就是一个很好的例子。定期地清除大量数据时,如果用一个大的语句一次性完成的话,则可能需要一次锁住所有数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但重要的
查询。将一个大的DELETE语句切分成多个较小的查询可以尽可能小的影响mysql性能,同时还可以减少Mysql复制的延迟。
在这里插入图片描述
一次性删除一万行数据一般来说是一个比较高效而且对服务器影响也最小的做法,同时,需要注意的是,如果每次删除数据后,都暂停一会儿在做下一次删除,这样也可以将服务器上原来一次性的压力分散到一个很长的时间段中,就可以大大降低对服务器的影响,还可以大大减少删除时锁的持有时间。

(3)分解关联查询
用分解关联查询的方式重构查询有如下的优势:
a. 让缓存的效率更高
b. 将查询分解后,执行单个查询可以减少锁的竞争
c. 在应用层做关联,可以更容易对数据库进行拆分,更容易坐到高性能和可扩展
d. 查询本身效率也可能会有所提升
e. 可以减少冗余记录的查询

四、查询执行的基础

当我们向mysql发送一个请求的时候,mysql到底做了些什么:
在这里插入图片描述

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

(1)mysql客户端/服务器通信协议
mysql的客户端和服务器之间的通信协议是“半双工的”, 这意味着,在任何一个时刻,这两个动作不能同时发生,所以我们无法也无须将一个消息切成小块独立来发送。

(2)查询缓存
在解析一个查询语句之前,如果查询缓存是打开的,那么mysql会优先检查这个查询是否命中了查询缓存中的数据,这个检查是通过一个对大小写敏感的哈希查找实现的。查询和缓存的中的查询即使只有一个字节不同,那也不会匹配缓存结果。

(3)查询优化处理

1. 语法解析器和预处理
首先,mysql通过关键字将SQL语句进行解析,并生成一棵对应的“解析树”。mysql解析器将使用mysql语法规则验证和解析查询。例如,它将验证是否使用错误的关键字,或者使用关键字的顺序是否正确等,再或者它还会验证引号是否能前后正确匹配。

预处理器则会根据一些Mysql规则进一步检查解析树是否合法,例如,这里将检查数据表和数据列是否存在,还会解析名字和别名,看看它们是否有歧义。
下一步预处理器会验证权限,这通常很快,除非服务器上有非常多的权限配置。

2. 查询优化器
mysql使用基于成本的优化器,它将尝试预测一个查询使用某种执行计划时的成本,并选择其中成本最小的一个。
有很多原因会导致mysql优化器选择错误的执行计划,如下所示:
(1)统计信息不准确
(2)执行计划中的成本估算不等同于实际执行的成本
(3)Mysql的最优可能和你想的最优不一样
(4)mysql从不考虑其他并发执行的查询,这可能会影响到当前查询的速度
(5)mysql也并不是任何时候都是基于成本的优化,有时也会基于一些固定的规则
(6)mysql不会考虑不受其控制的操作的成本,例如执行存储过程中或者用户自定义函数的成本
mysql的查询优化器是一个非常复杂的部件,它使用了很多优化策略来生成一个最优的执行计划,优化策略可以简单地分为两种,一种是静态优化,一种是动态优化。静态优化可以直接对解析树进行分析,并完成优化。
相反,动态优化则和查询的上下文有关,也可能和很多其他因素有关,例如where条件中的取值,索引中条目对应的数据行数等,这需要在每次查询的时候都重新评估,可以认为这是“运行时优化”。

下面是一些mysql能够处理的优化类型:
(1)重新定义关联表的顺序
数据表的关联并不总是按照在查询中指定的顺序进行。决定关联的顺序是优化器很重要的一部分功能
(2)将外连接转化成内连接
(3)使用等价变换规则
mysql可以使用一些等价变换来简化并规范表达式,它可以合并和减少一些比较,这可以移除一些恒成立和一些恒不成立的判断。
(4)优化count()、min()、max()
索引和列是否可以为空通常可以帮助mysql优化这类表达式
例如,要找到某一列的最小值,只需要查询对应B-Tree索引最左端的记录,mysql可以直接获取索引的第一行记录,在优化器生成执行计划的时候就可以利用这一点,在B-Tree索引中,优化器会将这个表达式作为一个常数对待。
(5)预估并转化为常数表达式
当mysql检测到一个表达式可以转化为常数的时候,就会一直把该表达式作为常数进行优化处理
(6)覆盖索引扫描
当索引中的列包含所有查询中需要使用的列的时候,mysql就可以使用索引返回需要的数据,而无须查询对应的数据行
(7)子查询优化
mysql在某些情况下可以将子查询准换一种效率更高的形式,从而减少多个查询多次对数据进行访问
(8)提前终止查询
在发现已经满足查询需求的时候,mysql总是能够立刻终止查询
(9)等值传播
如果两个列的值通过等式关联,那么mysql能够把其中一列的where条件传递到另一列上。
(10)列表in()的比较
mysql将in()列表中的数据先进行排序,然后通过二分查找的方式来确定列表中的值是否满足条件,这是一个O(logN)复杂度的操作,等价地转换成or查询的复杂度为O(N), 因此,对于IN()列表中有大量取值的时候,mysql的处理速度将会更快。

3. 数据和索引的统计信息
因为服务器层没有任何统计信息,所以Mysql查询优化器在生成查询的执行计划时,需要向存储引擎获取相应的统计信息,存储引擎则提供给优化器对应的统计信息,包括:每个表获取索引有多少个页面、每个表得每个索引的基数是多少、数据行和索引长度、索引的分布信息等,优化器会根据这些信息来选择一个最优的执行计划。

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

5. 执行计划
mysql生成查询的一棵指令树,然后通过存储引擎完成这棵指令树并返回结果。

6. 关联查询优化器
关联优化器会尝试在所有的关联顺序中选择一个成本最小的来生成执行计划树,如果可能,优化器会遍历每一个表然后逐个做嵌套循环计算每一颗树可能的执行计划树的成本,最后返回一个最优的执行计划。不过,当需要关联的表过多时,优化器会选择贪婪算法的方式查找最优的关联顺序。

7.排序优化
如果需要排序的数据量小于“排序缓冲区”, mysql使用内存进行“快速排序”操作,如果内存不够排序,那么mysql会先将数据分块,对每个独立的块使用“快速排序”进行排序,并将各个块的排序结果存放在磁盘中,然后将各个排好序的块进行合并,最后返回排序结果。
mysql有如下两种排序算法:

  1. 两次传输排序
    读取行指针和需要排序的字段,对其进行排序,然后再根据排序结果读取所需要的数据行,这需要进行两次数据传输。
  2. 单次传输排序
    先读取查询所需要的所有列,然后再根据给定列进行排序,最后直接返回排序结果。
    很难说哪个算法效率更高,两种算法都有各自最好和最糟的场景,当查询需要所有列的长度不超过参数max_length_for_sort_data时,mysql使用“单次传输排序”,超过则使用“两次传输排序”。

(4)查询执行引擎
相对于查询优化阶段,查询执行阶段不是那么复杂:mysql只是简单地根据执行计划给出的指令逐步执行。

(5)返回结果给客户端
查询执行的最后一个阶段是将结果返回给客户端。即使查询不需要返回结果集给客户端,mysql仍然会返回这个查询的一些信息,如该查询影响到的行数。
如果查询可以被缓存,那么mysql在这个阶段也会将结果存放在查询缓存中。
mysql将结果集返回客户端是一个增量、逐步返回的过程。

五、Mysql查询优化器的局限性

(1)关联子查询
一般会建议使用左外连接(left outer join)代替子查询,理论上,改写后的Mysql的执行计划完全不会改变。
(2)union的限制
(3)索引合并优化
当where子句中包含多个复杂条件的时候,Mysql能够访问单个表的多个索引以合并和交叉过滤的方式来定位需要查找的行。
(4)等值传递
某些时候,等值传递会带来一些意想不到的额外消耗,例如,有一个非常大的in()列表,而mysql优化器发现存在where、on 或者using的子句,将这个列表的值和另一个表得某个列相关联。
(5)并行执行
mysql无法利用多核特性来并行执行查询。
(6)哈希关联
(7)松散索引扫描
由于历史原因,mysql并不支持松散索引扫描,也就无法按照不连续的方式扫描一个索引。
(8)最大值和最小值优化
(9)在同一个表上查询和更新
Mysql不允许对同一张表同时进行查询和更新,可以通过使用生成表的形式来绕过上面的限制。

六、优化特定类型的查询

(1)优化count()查询
如果想知道的是结果集的行数,最好使用count(),如果希望统计某个列值得数量,则使用count(col)。
一个容易产生的误解就是:MyISAM的count()函数总是那么快,不过也是有前提条件的,即只有没有任务where条件的count(
)才非常快,因为此时无须实际地去计算表得行数。Mysql可以利用存储引擎的特性直接获得这个值。如果mysql知道某列col不可能为Null值,那么Mysql内部会将count(col)表达式优化为count(*)。
(2)优化关联查询
确认on或者using子句的列上有索引
确保任何的group by和order by中的表达式只涉及到一个表中的列,这样mysql才有可能使用索引来优化这个过程。
(3)优化子查询
(4)优化group by 和distinct
在mysql中,当无法使用索引的时候,group by 使用两种策略来完成:
使用临时表或者文件排序来做分组,对于任何查询语句,这两种策略的性能都有可以提升的地方

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值