【MySQL】查询性能优化

目录

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

重构查询的方式

MySQL查询优化器的局限性


如果查询写得很糟糕,即使库表结构再合理、索引再合适,也无法实现高性能。

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

查询性能低下最基本的原因是访问的数据太多。

对于低效的查询,从以下两个分析很有效:

  1. 确认应用程序是否在检索大量超过需要的数据。这意味着访问了太多的行,但有时候也可能也是访问了太多的列。
  2. 确认MySQL服务器层是否在分析大量超过需要的数据行。

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

查询不需要的额记录

一个常见错误是误以为MySQL会只返回需要的数据,实际上MySQL却是先返回全部结果集再进行计算。实际情况是MySQL会查询出全部的结果集,客户端的应用程序会接受全部的结果集数据,然后抛弃其中大部分数据。最简单的解决办法是在这样的查询后加上LIMIT。

多表关联时返回全部列

【MySQL】查询性能优化_第1张图片

 

这将返回这三个表的全部数据列。正确的方式应该是像下面这样只取需要的列

 

总是取出全部列

每次看到SELECT*时需要小心是否会返回全部的列。取出全部列,会让优化器无法完成索引覆盖扫描这类优化,还会为服务器带来额外的I/O、内存和CPU消耗。

重复查询相同的数据

不断重复执行相同的查询,每次都返回相同的数据,比较好的方案是,当初次查询的时候将这个数据缓存起来,需要的时候从缓存中取出。

MySQL是否在扫描额外的记录

衡量查询开销的三个指标

  1. 响应时间
  2. 扫描的行数
  3. 返回的行数

响应时间

响应时间是服务时间和排队时间之和

扫描的行数和返回的行数

并不是所有的行的访问代价都是相同的。较短的行访问速度更快,内存中的行也比磁盘中的行访问速度更快。

理想状态下扫描行数和返回行数应该相同,实际在做一个关联查询时,服务器必须要扫描多行才能生成结果集中的一行。

扫描的行数和访问类型

EXPLAIN语句中的type列反映了访问类型。从全表扫描到索引扫描、范围扫描、唯一索引扫描、常数引用,上述速度由慢到快。

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

Using Where表示MySQL通过WHERE条件来筛选存储引擎返回的记录。

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

  1. 在索引中使用WHERE条件来过滤不匹配的记录
  2. 使用索引覆盖扫描来返回记录,直接从索引中过滤不需要的记录并返回命中结果。
  3. 从数据表中返回数据,然后过滤不满足条件的记录。

如果发现查询需要扫描大量的数据但只返回少数的行,那可以用如下方法优化:

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

重构查询的方式

切分查询

将大查询切分成小查询,每个查询功能完全一样,只完成一小部分,每次只返回一部分查询结果。

分解关联查询

优势:

  1. 让缓存的效率更高
  2. 将查询分解后,执行单个查询可以减少锁的竞争
  3. 在应用层做关联,可以更容易对数据库进行拆分,更容易做到高性能和可拓展
  4. 查询本身效率会有提升
  5. 可以减少冗余记录的查询

查询执行的基础

流程:

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

【MySQL】查询性能优化_第2张图片

 

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

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

客户端使用一个单独的数据包将查询传给服务器。max_allowed_packet限制了服务端接收的数据大小,如果超过会抛出异常。客户端一旦发送了请求就只能等待结果

服务器想给给客户端的数据通常很多,由多个数据包组成。当服务器开始响应客户端的时候,客户端必须完整地接收整个返回结果。当服务器生成第一个结果的时候就已经向客户端发送。等所有数据都已经发送给客户端才能释放这条查询所占用的资源

查询状态

  1. Sleep:线程正在等待客户端发来的新请求。
  2. Query:线程正在执行查询或者正在将结果发送给客户端。
  3. Locked:在MySQL服务器层,该线程正在等待表锁。在存储引擎级别实现的锁,例如InnoDB的行锁,并不会体现在线程状态中。对于MyISAM来说这是一个比较经典的状态。
  4. Analyzing and statistics:线程正在收集存储引擎的统计信息,并生成查询的执行计划。
  5. Coping to tmp table [on disk]:线程正在执行查询,并且将其结果集都复制到一个临时表中。要么是GROUP BY操作,要么是文件排序操作,要么是UNION操作。如果有”on disk“说明将临时表存储在磁盘上。
  6. Sorting result:线程正在对结果集进行排序。
  7. Sending data:线程在多个状态之间传送数据,或者生成结果集,或者在向客户端返回数据。

查询缓存

通过一个对大小写敏感的哈希查找实现,查询是否命中查询缓存中的数据。

如果命中了查询缓存,并且返回查询结果之前MySQL会检查一次用户权限。如果满足权限,无须解析、生成执行计划、执行SQL语言等操作,直接从缓存中获结果并返还给客户端。

查询优化处理

语法解析器和预处理

MySQL通过关键字将SQL语句进行解析,生成一颗对应的”解析树”。MySQL解析器将使用MySQL语法规则验证和解析查询。它将验证是否使用错误的关键字,或者使用关键字的顺序是否正确等,再或者它还会验证引号是否能前后正确匹配。

预处理器则根据MySQL规则进一步检查解析树是否合法,下一步预处理器会验证权限。

查询优化器

优化器将语法树转换成执行计划,从很多种执行方式中,找到最好的计划。优化器在评估成本的时候并不考虑任何层面的缓存,假设读取任何数据都需要一次磁盘I/O。

导致MySQL优化器选择错误执行计划的原因:

  1. 统计信息不准确。从存储引擎提供的统计信息不准确。比如InnoDB因为其MVCC的架构,并不能维护一个数据表的行数的精确统计信息。
  2. 执行计划中的成本估算不等同于实际执行的成本。
  3. 根据成本选择的最优执行计划,并不一定是时间最短的。
  4. 不考虑并发执行的计划。

静态优化

 静态优化可以直接对解析树进行分析,并完成优化,静态优化在第一次完成后就一直有效,即使使用不同的参数重复执行查询也不会发生变化

动态优化

 和查询的上下文有关,也可能和很多其他因素有关,动态优化则在每次执行时都需要重新评估。

下面是一些MySQL能够处理的优化类型

  1. 重新定义关联表的顺序
  2. 将外连接转换成内连接
  3. 使用等价变换规则
  4. 优化COUNT()、MIN()和MAX()

索引和列是否可为空通常可以帮助MySQL优化这类表达式。例如:没有WHERE条件的COUNT(*)查询通常可以使用存储引擎提供的一些优化。

  1. 预估并转化为常数表达式

一个用户自定义变量在查询中没有发生变化时就可以转换成一个常数。

主键或者唯一键查找语句也可以转换为常数表达式。

  1. 覆盖索引扫描
  2. 子查询优化
  3. 提前终止查询

在发现已经满足查询需求的时候,MySQL总是能够立刻终止查询。一个典型的例子就是当使用了LIMIT子句的时候。

  1. 等值传播

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

  1. 列表IN()的比较

MySQL将IN()中的数据先进行排序,然后通过二分查找的方式来确定列表中的值是否满足条件。而使用OR查询的复杂度为O(n)。

数据和索引的统计信息

统计信息由存储引擎实现,不同的存储引擎可能会存储不同的统计信息(Archive引擎,则根本就没有存储任何统计信息)

MySQL如何执行关联查询

MySQL认为任何一个查询都是一次”关联“(例:每一个查询,每一个片段:子查询、基于表单的select),并不仅仅是一个查询需要到两个表匹配才叫关联。

UNION查询:

将一系列的单个查询结果放到一个临时表(临时表中没有任何索引)中,然就重新读出临时表数据来完成UNION查询。读取结果临时表也是一次关联。

关联执行的策略:

MySQL对任何关联都执行嵌套循环关联(嵌套查询,回溯)操作。

【MySQL】查询性能优化_第3张图片

 

FROM中子查询:

先执行子查询并将结果放到一个临时表中,将整个表当作一个普通的表对待。

MySQL如何实现多表关联:

【MySQL】查询性能优化_第4张图片

 

排序优化

文件排序:如果不能使用索引生成排序结果的时候,MySQL需要自己进行排序。

如果需要排序的数据量小于”排序缓冲区“,在内存进行”快速排序“操作。

如果内存不够排序,那么MySQL会先将数据分块,对每个独立的块使用”快速排序“进行排序,将各个块的排序结果存放在磁盘上,然后将各个排好序的块进行合并,最后返回排序结果。

排序的算法:

两次传输排序(旧版本)

第一次:读取行指针和需要排序的字段,对其进行排序。第二次:然后再根据排序结果读取所需要的数据行。

第二次读取会产生大量的随机I/O,所以两次传输的成本非常高。MyISAM非常依赖操作系统对数据的缓存—排序缓冲区。

单次传输排序

先读取查询所需要的所有列,然后根据给定列进行排序,直接返回结果。

只需要一次顺序I/O,无须任何随机I/O。返回的列非常多,非常大,会占用额外的空间。

返回结果给客户端

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

MySQL查询优化器的局限性

关联子查询

MySQL子查询非常糟糕,最糟糕的一类是WHERE条件中包含IN()的子查询语句。,例如,我们希望找到Sakila数据库中,演员Penelope Guiness(他的actor_id为1)参演过的所有影片信息。

【MySQL】查询性能优化_第5张图片

 

然而MySQL会将相关的外层表压到子查询中,他认为这样可以更高效率的查找数据行,也就会变成下面的样子:

【MySQL】查询性能优化_第6张图片

 

这时,MySQL先选择对file表进行全表扫描,然后根据返回的film_id逐个执行子查询。如果外层的表是一个非常大的表,那么这个查询的性能会非常糟糕。

松散索引扫描

MySQL并不支持松散索引扫描

存在索引(a,b),执行语句select ... from t where b between 2 and 3

全表扫描:

【MySQL】查询性能优化_第7张图片

 

松散索引扫描

【MySQL】查询性能优化_第8张图片

 

最大值和最小值优化

使用LIMIT来进行优化

 

 

在同一个表上查询和更新

【MySQL】查询性能优化_第9张图片

【MySQL】查询性能优化_第10张图片 

 

MySQL不允许对同一张表同时进行查询和更新

但可以通过生成表的形式来绕过上面的限制,因为MySQL只会把这个表当作一个临时表来处理。

优化特定类型的查询

优化COUNT()查询

COUNT()的作用

  1. 统计某个列值的数量,也可以统计行数。在统计列值时要求列值是非空的
  2. 统计结果集的行数

关于MyISAM的神话

即只有没有任何WHERE条件的COUNT(*)才非常快,因为此时无须实际地去计算表的行数。MySQL可以利用存储引擎的特性直接获得这个值。如果MySQL知道某列col不可能为NULL值,那么MySQL内部会将COUNT(col)表达式优化为COUNT(*)。

优化方法

  1. 使用近似值。
  2. 使用索引覆盖扫描
  3. 利用MyISAM在COUNT(*)全表非常快的特性,加速一些特定条件的COUNT()查询

优化关联查询

  1. 确保ON或者USING子句中的列上有索引。在创建索引的时候就要考虑到关联的顺序。 当表A和表B用列c关联的时候,如果优化器的关联顺序是B、A,那么就不需要在 B表的对应列上建上索引。没有用到的索引只会带来额外的负担。一般来说,只需要在关联顺序中的第二个表的相应列上创建索引。
  2. 确保任何的GROUP BY和ORDER BY中的表达式只涉及到一个表中的列,这样MySQL才有可能使用索引来优化这个过程。

优化子查询

尽可能使用关联查询代替

优化GROUP BY和DISTINCT

  1. 可以使用索引来优化。
  2. 采用标识列做分组的效率比较高.
  3. 如果没有用过ORDER BY子句显示地指定排序列,当查询使用GROUP BY子句的时候,结果集会自动按照分组的字段进行排序。可能会导致文件排序,可以使用ORDER BY NULL,让MySQL不再进行问及那排序,或者使用DESC、ASC是分组按照需要的方向排序。

优化LIMIT分页

  1. 使用索引覆盖扫描,而不是查询所有的列。避免访问过多无用数据。然后根据需要做一次关联操作再返回所需的列。
  2. 借助主键是单调递增的,使用where进行限制。

优化UNION查询

  1. 尽量使用UNION ALL。否则会在临时表上加上DISTINCT选项,这会导致对整个临时表的数据做唯一性检查,没有索引,代价很高。

使用用户自定义变量

用户自定义变量是一个用来存储内容的临时容器,在连接MySQL的整个过程中都存在

可以使用下面的SET和SELECT语句来定义他们

 

  1. 不能使用用户自定义变量的场景:
  2. 使用自定义变量的查询,无法使用查询缓存。
  3. 不能在使用常量或者标识符的地方使用自定义变量,例如表名、列名和LIMIT子句中。
  4. 用户自定义变量的生命周期是在一个连接中有效,所以不能用它们来做连接间的通 信。
  5. 如果使用连接池或者持久化连接,自定义变量可能让看起来毫无关系的代码发生交 互(如果是这样,通常是代码bug或者连接池bug,这类情况确实可能发生)。
  6. 在5.0之前的版本,是大小写敏感的,所以要注意代码在不同MySQL版本间的兼容性问题。
  7. 不能显式地声明自定义变量的类型。

MySQL版本中也可能不-样。如果你希望变量是整数类型,那么最好在初始化的时 候就赋值为0,如果希望是浮点型则赋值为0.0,如果希望是字符串则赋值为",用 户自定义变量的类型在赋值的时候会改变。MySQL的用户自定义变量是一个动态类型。

  1. MySQL优化器在某些场景下可能会将这些变量优化掉,这可能导致代码不按预想的方式运。
  2. 赋值的顺序和赋值的时间点并不总是固定的,这依赖于优化器的决定。实际情况可 能很让人困,后面我们将看到这一点。
  3. 赋值符号:=的优先级非常低,所以需要注意,赋值表达式应该使用明确的括号。

使用未定义变量不会产生任何语法错误,如果没有意识到这一点,非常容易犯错。

作用

  1. 优化排名语句
  2. 避免重复查询刚刚更新的数据
  3. 统计更新和插入的数量
  4. 确定取值的顺序

其他用处

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

你可能感兴趣的:(MySQL,mysql,数据库,服务器)