设计优秀的库表结构及索引对于高性能来说必不可少,但这还不够。
糟糕的查询往往不能发挥索引的功效,也就达不到所谓的高性能。
要想实现高性能,库表结构优化,索引优化,查询优化 需要齐头并进。
为什么查询速度会很慢?
查询的生命周期大致可以按照顺序来看:
从客户端,到服务器,然后在服务器进行解析,生成执行计划,执行,并返回结果给客户端。
其中“执行”可以认为是整个周期里最重要的一个阶段,其中包含了大量为了检索数据到存储引擎的调用以及调用后的处理,包括排序,分组等。
查询需要在不同的地方花费时间,包括网络,CPU计算,生成统计信息和执行计划,锁等待…。
了解查询的生命周期,清楚时间消耗在哪里,对于查询优化意义重大。
慢查询基础
优化数据访问
查询性能低下最基本的原因是:访问的数据太多,有些查询虽然返回的数据不多,但是底层可能需要访问大量的数据来做筛选。
大部分性能低下的查询可以通过减少数据访问量来进行优化。
- 确认应用程序是否检索了大量的数据行。
- 确认MySQL服务器是否在分析大量超过需要的数据行。
只请求有用的数据
有些查询会请求多余的数据,在应用程序中这些数据会被丢弃,这不仅给MySQL服务器带来不必要的负担,还增加了网络开销,CPU和内存开销。
要想达到高性能的查询,应该时常检查是否犯以下错误:
-
查询不需要的记录
只查询需要用到的数据行,例如分页查询时,请求MySQL时就应该分页,而不是在应用程序中做分页。扫描二维码关注公众号,回复: 8714330 查看本文章 -
多表关联时返回全部列
-
取出所有的列
虽然SELECT * 写起来很简单,但是每次使用时都需要好好思考,是否真的需要取出所有列?
取出所有列会让覆盖索引失效,给服务器带来不必要的I/O,内存和CPU消耗。
一些DBA是严格禁止使用SELECT * 的。 -
重复查询相同的数据
如果一些查询总是返回相同的数据,那么应该考虑使用数据缓存。
是否扫描了额外的记录
对于MySQL,衡量查询开销的三个指标:
- 响应时间
- 扫描的行数
- 返回的行数
响应时间
响应时间包括服务时间和等待时间,等待时间是不确定的,包括磁盘I/O,等待行锁等。
在高并发下,等待时间可能就会比较长,但是服务时间和查询设计的好坏有直接关系。
扫描和返回的行数
分析查询时,查看扫描的行数是非常有帮助的,可以在一定程度上说明该查询的效率是否高效。
理想情况下,扫描的行数 = 返回的行数。实际情况下,能做到这一点的查询并不多。
应该最大程度的减少存储引擎扫描的数据行。
访问类型
扫描的行数应该越少越好,访问类型也应该越高效越好。
同样的行,不同的访问类型,扫描的代价是不一样的。
全表扫描 < 范围扫描 < 唯一索引查询 < 常数引用等。
应该尽量避免全表扫描,性能是最差的,最简单的办法就是建立索引。
如果发现查询需要扫描大量的行,可以尝试这样优化:
- 使用索引覆盖扫描
- 改变表结构,使用汇总表
- 重写这个复杂的查询
重构查询的方式
查询一个结果集可以有很多种方式,我们应该尽力找到性能更好的方式。
一个复杂查询还是多个简单查询
如果一个复杂查询效率很低时,应该考虑是否可以拆分成多个简单的查询。
传统实现中,人们总是强调让数据库层做尽可能做多的工作,因为网络通信和数据库连接是一件代价很高的事情。
MySQL已经让连接和断开连接都很轻量级了,返回一个小的结果集也很高效。
某些MySQL版本在一个通用服务器上可以运行每秒10万次的查询。
现在网络的带宽和延迟也比以前好了很多,所以现在运行多个小查询已经不是问题。
不建议过量使用小查询,但如果拆分成小查询性能比一个复杂查询更好时,就大胆的使用。
切分查询
对于一个很大的查询,有时可以考虑切分成多个小查询。
每个小查询功能一样,只是负责返回部分数据。
删除数据就是一个很好的例子,如果直接DELETE一张大表,可能会一次性锁住很多数据,占满事务日志,耗尽系统资源,阻塞其他查询等。
将一个大的DELETE切分,每次只删除一万条,是一个比较高效且对MySQL影响很小的做法,可以大大减少删除时锁的持有时间。
分解关联查询
很多高性能的应用会对关联查询进行分解。
可以对每张表进行一次单表查询,然后在应用程序中进行关联。
- 让缓存效率更高
- 查询分解后,减少锁的竞争
- 在应用层做关联,可以更好的对数据库进行拆分
- 查询本身效率可能会有所提升
- 减少冗余记录的查询
查询执行的基础
如果想优化查询,那么前提必须弄清楚MySQL是如何优化和执行查询的。
当我们向MySQL发出一个请求时,MySQL到底做了些什么?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4Gepjek6-1573983731344)(https://i.niupic.com/images/2019/10/04/_27.png)]
- 客户端发送一条查询命令给服务器。
- 服务器先检查查询缓存,如果命中缓存,直接返回缓存中的数据,否则进行下一步。
- 服务器进行SQL解析,预处理,再由优化器生成对应的执行计划。
- MySQL根据优化器生成的存储计划调用存储引擎的API来执行查询。
- 将结果返回给客户端。
这只是一个大概的流程,实际上每一步都比想象中要复杂,查询优化器是特别复杂和难以理解的。
MySQL客户端/服务器通信协议
不需要去理解MySQL内部通信的细节,了解其大致是如何工作的即可。
MySQL客户端和服务端之间的通信是“半双工”的。
在任何一个时刻,要么是客户端向服务端发送数据,要么是服务端向客户端发送数据,两者不能同时进行。
这种协议让MySQL通信变得简单快速,但是也有一些限制。
任何一端,都必须接收完对方发送的完整消息才能进行响应。
客户端用一个单独的数据包将查询发送给服务器,所以当查询的语句很长时,参数MAX_ALLOWED_PACKET就特别重要了。
一旦客户端发出请求,它能做的只有等待结果响应。
查询状态
对于一个连接或者说一个线程,任何时候都有一个状态,该状态表示MySQL当前正在做什么。
使用SHOW FULL PROCESSLIST命令可以查看当前状态,Command列表示状态。
MySQL官方手册中对状态的解释:
-
Sleep
线程正在等待客户端发出新的请求。 -
Query
线程正在执行查询或者正在将结果返回给客户端。 -
Locked
在MySQL服务器层,该线程正在等待表锁。
在存储引擎级别实现的锁,不会体现在状态中。 -
Analyzing and statistics
线程正在收集存储引擎的统计信息,并生成查询的执行计划。 -
Copying to tmp table[on disk]
线程正在执行查询,并将结果集复制到一个临时表中。
这种状态一般是在做GROUP BY、文件排序、UNION。 -
Sorting result
线程正在对结果集排序。 -
Sending data
有多种情况:线程在多个状态之间传送数据、在生成结果集、在向客户端发送数据。
在一个繁忙的服务器上,可能看到大量的不正常状态,这通常表示某个地方有异常了。
查询缓存
如果查询缓存是打开的,那么MySQL在解析一个查询语句之前,会先检查是否命中缓存中的数据。
检查是通过一对大小写敏感的哈希查找实现的。查询和缓存中的内容即使只有一个字节不同,那也会进入下一阶段处理。
如果当前查询恰好命中了缓存,在返回结果前MySQL会检查一次用户权限,如果权限没问题,MySQL就会跳过所有其他阶段,直接从缓存中读取数据返回给客户端。
这种情况下,查询不会被解析,也不会生成执行计划,不会被执行,也不用和存储引擎API交互。
查询优化处理
如果没有查询到缓存,下一步MySQL将会将SQL转换成一个执行计划,MySQL再依照这个执行计划和存储引擎进行交互。这包含多个子阶段:解析SQL、预处理、优化SQL执行计划。
这个过程发生任何错误都可能终止查询,例如:SQL语法错误。
语法解析器和预处理
MySQL首先通过关键字将SQL语句进行解析,并生成一棵对应的“解析树”。
MySQL解析器校验SQL语法是否正确,包括:使用错误的关键字、关键字顺序不对、表名列名错误冲突…。
SQL语法校验完毕后,下一步预处理器会验证权限。
查询优化器
如果SQL语法和用户权限都没问题,那么将由优化器来生成执行计划。
一条查询可以有多种执行计划,返回的结果都相同,优化器的作用是找到最好的执行计划。
MySQL使用基于成本的优化器,它会预测某种执行计划的成本,然后使用成本最低的一个。
可以通过查询当前会话的 Last_query_cost 值来得知MySQL计算的查询成本。
SELECT SQL_CACHE count(*) FROM person;
SHOW STATUS LIKE 'Last_query_cost';
-- Last_query_cost = 1.199000
表示MySQL的优化器认为大概需要做1.199个数据页的随机查找才能完成查询。
这是根据一系列的统计信息计算得来的,包括:表或索引的页面个数、索引的基数、索引和数据行的长度、索引分布情况。
优化器在评估成本的时候,不会考虑任何缓存,它假设读取任何数据都需要一次I/O。
有多种原因导致优化器选择错误的执行计划:
- 统计信息不准确。优化器依赖于存储引擎提供的统计信息来评估成本,有的引擎提供的准确,有的则偏差很大。InnoDB由于MVCC导致不能维护一个表的行数的精确统计信息。
- 执行计划中的成本估算不等于实际成本。即使统计信息准确,优化器给出的执行计划也可能不是最优的。例如:虽然有的计划需要读取更多的数据页,但是如果数据页是顺序读或者已经存在于内存中了,那么其实它的访问成本更小。
- 优化器的最优和你想的最优可能不一样。你心中的最优可能是指执行时间最短,但优化器只基于模型选择最优执行计划,有时这并不是执行最快的方式。
- 优化器从不考虑并发查询,这可能会影响当前查询的速度。
- 如果存在全文搜索的MATCH()子句,那么优化器就会使用全文索引,即使其他索引可能执行成本更低。
- 优化器不会考虑不受其控制的成本。例如:执行存储过程或用户自定义函数。
- 优化器有时无法去估算所有可能的执行计划,所以可能会错过最优的执行计划。
MySQL的查询优化器是一个非常复杂的部件,它使用了很多优化策略来生成最优的执行计划。
简单可分为两种:
-
静态优化
直接对解析树进行分析,并完成优化。
例如:可以通过一些简单的代数变换将WHERE条件转换成另一种等价形式。
静态优化在第一次完成后就一直有效,可以认为是一种“编译时优化”。 -
动态优化
与查询的上下文有关,也可能和其他因素有关。
例如:WHERE条件中的取值、索引条目对应的数据行数等。
需要在每次查询时重新评估,可以认为是“运行时优化”。
MySQL能够处理的优化类型:
-
重新定义表的顺序
-
将外连接转换成内连接
-
使用等价变换规则
-
优化COUNT()、MIN()、MAX()
有些存储引擎如MyISAM会维护一个变量来记录数据行数,就不用再查询count(*)了。
B-Tree索引由于顺序存放,首节点就是MIN(),尾节点是MAX()。 -
预估并转换成常数表达式
-
覆盖索引扫描
-
子查询优化
-
提前终止查询
在发现已经满足查询需求时,MySQL总是能够立刻终止查询。
例如:WHERE条件中不可能成立时。
EXPLAIN
SELECT SQL_CACHE count(*) FROM person WHERE id = '1' AND id = '2';
-- Extra = Impossible WHERE
-
等值传播
如果两个列的值通过等式关联,MySQL会把其中一个列的WHERE条件传递到另一个列上。 -
列表IN()的比较
在很多数据库中,IN()等同于多个OR子句。
在MySQL中这点是不成立的,MySQL会将IN()里的数据先排序,然后通过二分法查找来判断列表中的值是否满足条件。
除此之外MySQL优化器还会做大量的优化,大多数时候让优化器自己去工作就可以了。
当优化器不能给出最优解时,可以再查询中添加hint提示,干扰优化器的行为。
数据和索引的统计信息
服务器层没有任何统计信息,MySQL优化器在生成执行计划时,需要向存储引擎获取相应的统计信息。
统计信息包括:表或索引有多少页面、索引的基数、索引的长度、索引的分布情况、数据行数等。
优化器根据这些信息来选择一个最优的执行计划。
执行计划
MySQL生成查询的一棵指令树,然后通过存储引擎完成这棵树并返回结果。
最终的查询计划包含了重构查询的全部信息。
查看最终的执行计划
通过对查询执行EXPLAIN EXTENDED后,再执行SHOW WARNINGS可以看到优化后的具体查询:
EXPLAIN EXTENDED
SELECT count(*) FROM person;
SHOW WARNINGS;
-- /* select#1 */ select count(0) AS `count(*)` from `test`.`person`
关联查询优化器
关联查询优化是MySQL查询优化器很重要的一部分,它决定了多个表关联时的顺序。
不同的关联顺序查询的性能是不一样的,优化器旨在找出性能最好的顺序。
如果优化器给出的顺序不好,可以通过关键字STRAIGHT_JOIN重写查询。
排序优化
排序是一个代价很高的操作,应尽可能的避免对大量数据排序。
当不能使用索引生成排序结果时,MySQL就要自己排序了。
数据量小直接在内存中进行,数据量太大就只能在磁盘中进行。
MySQL将这个过程统称为:文件排序(filesort)。
如果需要排序的量小于“排序缓冲区”,MySQL就使用内存进行快速排序。
如果内存不够排序,就先将数据分块,对独立的块进行排序,然后放到磁盘中,最终将各个排好序的结果进行合并返回。
MySQL有两种排序算法:
- 两次传输排序(旧版本)
- 单词传输排序(新版本)
当查询需要的列长度不超过参数max_length_for_sort_data时,使用单词传输排序,否则使用两次传输排序。
查询执行引擎
相较于查询优化阶段,查询执行阶段不是那么复杂。
MySQL只是简单的根据执行计划给出的指令逐步执行。
在执行的过程中,有大量的操作需要调用存储引擎实现的接口来完成。
返回结果
查询执行的最后一个阶段就是将结果返回给客户端。
如果查询可以被缓存,那么MySQL会把结果放到查询缓存中。
MySQL将结果集返回给客户端是一个增量,逐步返回的过程。
一旦MySQL开始生成第一条结果时,就可以开始逐步返回给客户端了。
无需全部生成再一次性返回,使得服务器无需存储太多的结果,也可以让客户端第一时间获取到结果。
MySQL查询优化器的局限性
MySQL的“嵌套循环”并不是对每种查询都是最优的。
查询优化器的提示(hint)
如果对查询优化器的执行计划不满意,可以使用几个提示来控制最终的执行计划。
-
HIGH_PRIORITY和LOW_PRIORITY
当多个语句同时访问某一个表时,可以设置语句的优先级。 -
DELAYED
对于INSERT和REPLACE有效。MySQL会将使用该提示的语句立即返回给客户端,并将插入的数据缓冲到缓冲区,然后在表空闲时批量插入。
日志系统使用该提示十分有效。
INSERT DELAYED INTO person VALUES ('1','2');
-
STRAIGHT_JOIN
适用于SELECT,放在两张关联表之间,用于固定关联表的顺序。 -
SQL_SMALL_RESULT和SQL_BIG_RESULT
适用于SELECT,告诉MySQL返回的结果集会很小或很大。
针对分组或排序时,分别在内存或磁盘中进行。 -
SQL_BUFFER_RESULT
告诉优化器将查询结果放入临时表,并尽快释放表锁。 -
SQL_CACHE和SQL_NO_CACHE
告诉优化器查询结果是否需要放到查询缓存中。 -
SQL_CALC_FOUND_ROWS
让MySQL返回的结果集包含更多的信息。 -
FOR_UPDATE和LOCK IN SHARE MODE
给SELECT语句加上排它锁和共享锁。 -
USE INDEX、IGNORE INDEX、FORCE INDEX
USE INDEX和FORCE INDEX作用基本相同,都是告诉优化器使用什么索引。
FORCE INDEX会告诉优化器全表扫描的成本会远高于索引扫描。
IGNORE INDEX会忽略索引,告诉优化器不要走索引扫描。
其他一些参数也可以控制优化器的行为:
-
optimizer_search_depth
控制优化器在穷举执行计划时的限度。
如果查询长时间处于“Statistics”状态,可以考虑调低此参数。 -
optimizer_prune_level
默认是打开的,让优化器根据需要扫描的行数来决定是否跳过某些执行计划。 -
optimizer_switch
包含了一些开启/关闭优化器特性的标志位。
优化特性的查询
优化COUNT()查询
COUNT()聚合函数的优化,优化之前先了解一下。
COUNT()的作用
COUNT()是一个特殊的函数,有两种不同的作用:统计某个列值的数量、统计行数。
统计列值时,要求列值不是NULL。
为空时,就是统计行数。最简单的就是COUNT(*)。
简单的优化
有时可以利用MyISAM的COUNT(*)很快的特性,来加速一些特定条件的COUNT()查询。
例如:使用COUNT(*)去减去相反条件的COUNT()查询。
使用近似值
如果不要求完全精确的COUNT值,可以使用近似值来代替。
EXPLAIN出来的优化器估算的行数就是一个不错的近似值,执行EXPLAIN并不会真正的去执行查询,执行成本更低。
更复杂的优化
通常来说,COUNT()需要扫描大量的行才能精确获取结果,因此是很难优化的。
除了前面提到的方法,还有索引覆盖扫描。
除此之外别无他法,如果速度还不行,只能增加汇总表,或引用外部的缓存系统了。
优化关联查询
- 确保ON或者USEING子句中的列有用到索引。
- 确保任何GROUP BY和ORDER BY中的表达式只涉及到一个表中的列,这样MySQL才有可能使用索引来优化排序。
优化子查询
尽可能的使用关联查询代替子查询。
优化LIMIT分页
LIMIT分页在数据量大时,越到后面效率越低,因为需要扫描的数据行会越来越多。
优化分页最简单的方式就是尽可能的使用覆盖索引扫描,而不是查询所有的列。
例子:
SELECT * FROM person LIMIT 10000,10;
-- 应该改成如下
SELECT a.*
FROM person a
INNER JOIN ( SELECT id FROM person LIMIT 10000,10 ) b ON a.id = b.id;
如果已知分页的边界,可以使用BETWEEN。
使用自定义变量
用户自定义变量是一个用来存储内容的临时容器,在整个连接过程中都存在。
可以通过SET和SELECT来设置和查询:
SET @one := '1';
SELECT @one;
SELECT * FROM person WHERE id = @one;
属性和限制:
- 使用自定义变量查询,无法查询缓存。
- 不能在使用常量或标识符的地方使用,例如表名,列名。
- 变量只在当前连接中有效,不能和其他连接通信。
- 优化器可能会将变量优化掉。
- 赋值的顺序和时间不总是固定的。
- 使用未定义的变量不会有任何错误,没意识到这点容易犯错。