查询性能优化(高性能MySQL读书笔记)

优化数据访问

  1. 确认应用程序是否在检索大量超过需要的数据。这通常意味着访问了太多的行,或访问了太多的列。

  2. 确认MySQL服务器层是否分析大量找过需要的数据行。

向数据库请求不需要的数据

  • 查询不需要的记录

  • 多表关联时返回全部列

  • 总是取出全部列

  • 重复查询相同的数据

MySQL扫描额外的记录

衡量查询开销的指标

  • 响应时间

    服务时间和排队时间之和。

    服务时间:数据库处理这个查询真正花了多长时间。

    排队时间:服务器因为等待某些资源而没有真正执行查询的时间(例如:I/O操作、行锁等)。

  • 扫描的行数和返回的行数

type对应了访问的类型

从全表扫描到索引扫描、范围扫描、唯一索引查询、常数引用等。这里列的这些,速度是从慢到快,扫描的行数也是从小到大。

Using where,从好到坏

  • 在索引中使用WHERE条件来过滤不匹配的记录。在存储引擎层完成。

  • 使用索引覆盖扫描(Extra列中出现了 Using index)来返回记录,直接从索引中过滤不需要的记录并返回命中结果。在MySQL服务器层完成,无须回表

  • 从数据表中返回数据,然后过滤不满足条件的记录(Extra列中出现Using where)。在MySQL服务器层完成,MySQL需要先从数据表中读出记录然后过滤。

重构查询的方式

  • 一个复杂查询还是多个简单查询

  • 切分查询。大查询切分成小查询,分而治之。

  • 分解关联查询。

    1. 让缓存的效率更高。

    2. 将查询分解后,执行单个查询可以减少锁的竞争。

    3. 查询本身效率也可能会有所提升。例如:使用in() 替代关联查询。

    4. 减少冗余记录的查询,在应用层做关联查询。

    5. 在应用中实现了哈希关联,而不是MySQL嵌套循环关联。

查询执行的基础

查询执行路径

  1. 客户端发送一条查询给服务器。

  2. 服务器先检查查询缓存,如果命中了缓存,则立刻返回存储在缓存中的结果。否则进入下一阶段。

  3. 服务器端进行SQL解析、预处理,再由优化器生成对应的执行计划。

  4. MySQL 根据优化器生成的执行计划,调用存储引擎的API来执行查询。

  5. 将结果返回给客户端。

查询性能优化(高性能MySQL读书笔记)_第1张图片

客户端/服务器通信协议

MySQL客户端和服务器之间的通信协议是半双工的。任意一个时刻,要么是服务器向客户端发送数据,要么是客户端向服务器发送数据。

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

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

查询状态

对于一个MySQL连接,或者说一个线程,任何时刻都有一个状态。使用show full processlist命令查看”Command“列。

  • Sleep:线程正在等待客户端发来的新请求。

  • Query:线程正在执行查询或者正在将结果发送给客户端。

  • Locked:在MySQL服务器层,该线程正在等待表锁。在存储引擎级别实现的锁,例如InnoDB的行锁,并不会体现在线程状态中。对于MyISAM来说这是一个比较经典的状态。

  • Analyzing and statistics:线程正在收集存储引擎的统计信息,并生成查询的执行计划。

  • Coping to tmp table [on disk]:线程正在执行查询,并且将其结果集都复制到一个临时表中。要么是GROUP BY操作,要么是文件排序操作,要么是UNION操作。如果有”on disk“说明将临时表存储在磁盘上。

  • Sorting result:线程正在对结果集进行排序。

  • Sending data:线程在多个状态之间传送数据,或者生成结果集,或者在向客户端返回数据。

查询缓存

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

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

查询优化处理

语法解析器和预处理

语法解析器

首先,MySQL通过关键字将SQL语句进行解析,并生成一颗对应的”解析树“。MySQL解析器将使用MySQL语法规则验证和解析查询。例如:是否使用错误关键字,关键字的顺序是否正确等

预处理

预处理器则根据一些MySQL规则进一步检查解析树是否合法。例如:数据表和数据列是否存在,解析名字和别名等。

验证权限

查询优化器

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

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

  • 统计信息不准确。从存储引擎提供的统计信息不准确。比如InnoDB因为其MVCC的架构,并不能维护一个数据表的行数的精确统计信息。

  • 执行计划中的成本估算不等同于实际执行的成本。

  • 根据成本选择的最优执行计划,并不一定是时间最短的。

  • 不考虑并发执行的计划。

静态优化(一次):

直接对解析树进行分析,并完成优化。例如:通过简单的代数变换将where条件转换成另一种等价的形式。

动态优化(每次执行):

和查询的上下文优化。例如:where条件中的取值,索引中条目对应的数据行数等。

MySQL能够处理的优化类型

  • 重新定义关联表的顺序

    数据表的关联并不是总按照查询中指定的顺序进行,而是和优化器有关。

  • 将外连接转换成内连接

  • 使用等价变换规则

  • 优化COUNT()、MIN()和MAX()

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

  • 预估并转化为常数表达式

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

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

  • 覆盖索引扫描

  • 子查询优化

  • 提前终止查询

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

  • 等值传播

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

  • 列表IN()的比较

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

MySQL如何执行关联查询

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

UNION查询

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

关联执行的策略

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

查询性能优化(高性能MySQL读书笔记)_第2张图片

FROM中子查询

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

MySQL如何实现多表关联

查询性能优化(高性能MySQL读书笔记)_第3张图片

排序优化

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

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

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

排序的算法

  • 两次传输排序(旧版本)

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

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

  • 单次传输排序(新版本)

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

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

关联查询时的文件排序

  • ORDER BY子句中的所有列都来自关联的第一个表,在处理第一个表的时候就会进行文件排序。

  • 除第一种情况之外,将关联的结果存放到一个临时表中,然后在所有的关联都结束后,在进行文件排序。

返回结果给客户端

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

MySQL将结果集返回客户端是一个增量、逐步返回的过程。例如:开始生成第一条结果时,MySQL就开始向客户按逐步返回结果集了。

优点

  • 服务端不会因为存储太多结果,消耗太多内存。结果集中的每一行都会以一个满足MySQL客户端/服务器通信协议的封包发送,在通过TCP协议进行传输,TCP可能会将封包进行缓存然后批量传输。

  • 使得客户端第一时间获得返回结果。

查询优化器的局限性

关联子查询

where中使用IN()的子查询语句

MySQL会将相关的外层表压到子查询中。

松散索引扫描

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

全表扫描:

查询性能优化(高性能MySQL读书笔记)_第4张图片

松散索引扫描:

查询性能优化(高性能MySQL读书笔记)_第5张图片

在同一个表上查询和更新

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

但是可以通过生成临时表打破限制。

生成临时表的方式

  • UNION

  • GROUP BY

  • 文件排序:ORDER BY、DISTINCT

  • FROM子查询,多表关联

查询性能优化(高性能MySQL读书笔记)_第6张图片

查询性能优化(高性能MySQL读书笔记)_第7张图片

优化特定类型的查询

优化COUNT()查询

COUNT()的作用

可以统计某个列值的数量,也可以统计行数。统计列值时要求列值时非空的。

  • 如果在COUNT()的括号中指定了列或者列的表达式,则统计的就是这个表达式有值的结果数。

  • 统计结果集的行数。COUNT(*)。

MyISAM的神话

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

优化方法

  1. 使用近似值。

  2. 使用索引覆盖扫描

  3. 利用MyISAM在COUNT(*)全表非常快的特性,加速一些特定条件的COUNT()查询。

优化关联查询

  • 确保ON或者USING子句中的列上有索引。在创建索引的时候就要考虑到关联的顺序。 当表A和表B用列c关联的时候,如果优化器的关联顺序是B、A,那么就不需要在 B表的对应列上建上索引。没有用到的索引只会带来额外的负担。一般来说,只需要在关联顺序中的第二个表的相应列上创建索引。

  • 确保任何的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的整个过程中都存在。

查询性能优化(高性能MySQL读书笔记)_第8张图片

不能使用用户自定义变量的场景

  • 使用自定义变量的查询,无法使用查询缓存。

  • 不能在使用常量或者标识符的地方使用自定义变量,例如表名、列名和LIMIT子句中。

  • 用户自定义变量的生命周期是在一个连接中有效,所以不能用它们来做连接间的通 信。

  • 如果使用连接池或者持久化连接,自定义变量可能让看起来毫无关系的代码发生交 互(如果是这样,通常是代码bug或者连接池bug,这类情况确实可能发生)。

  • 在5.0之前的版本,是大小写敏感的,所以要注意代码在不同MySQL版本间的兼容性问题。

  • 不能显式地声明自定义变量的类型。

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

  • MySQL优化器在某些场景下可能会将这些变量优化掉,这可能导致代码不按预想的方式运行。

  • 赋值的顺序和赋值的时间点并不总是固定的,这依赖于优化器的决定。实际情况可 能很让人困惑,后面我们将看到这一点。

  • 赋值符号:=的优先级非常低,所以需要注意,赋值表达式应该使用明确的括号。

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

作用

  • 优化排名语句。因为赋值的时间问题,可能需要使用子查询临时表。例如:select子句中进行赋值,然后再where子句中读取变量。

  • 避免重复查询刚刚更新的数据。

  • 统计更新和插入的数量。

    查询性能优化(高性能MySQL读书笔记)_第9张图片

  • 确定取值的顺序。

    select子句中进行赋值,然后再where子句中读取变量。

你可能感兴趣的:(MySQL)