本文为《高性能MySQL(第三版)》一书的摘要总结
如果要优化查询,实际上是要优化其子任务,要么要出其中一些子任务,要么减少子任务的执行次数,要么让子任务运行的更快。
查询性能低下最基本的原因是访问的数据太多。对于低效的查询,下面两个步骤的分析总是很有效:
如果我们不需要展示全部记录,最好在查询后加上LIMIT
。
例如,我们要查询一个员工的工号,姓名,和所以部门名称:
select
*
from
employees e
inner join dept_emp ed
using (emp_no)
inner join departments d
using(dept_no);
上面的查询会返回三个表的全部列,尽量不要这样查询,我们应该指定查询的列:
select
e.emp_no ,e.first_name ,e.last_name ,d.dept_name
from
employees e
inner join dept_emp ed
using (emp_no)
inner join departments d
using(dept_no);
每次看到SELECT *
时,都要用怀疑的眼光来审视。我们需要根据实际情况来判断是否真的需要查询所有的列,取出全部的列,我们无法完成 索引完全覆盖 这样的优化,还会对服务器资源带来额外消耗。
如果存在重复查询,我们可以将这个查询结果缓存。
在确定查询只返回需要的数据 之后,接下来应该看看查询为了返回这些结果是否扫描了过多的数据。
对于MySQL,最简单的衡量查询开销的三个指标:
这三个指标都会记录到慢日志中,所以 检查慢日志记录 是找出扫描行数过得的查询的好办法。
响应时间 = 服务时间 + 排队时间。
分析查询是,查看扫描的行数是非常有帮助的,这在一定程度上说明了该查询找到需要的数据的效率。
在EXPLAIN
语句中的type
反应了访问类型,访问类型有很多,从全表扫描到索引扫描、范围扫描、唯一索引查询、常数引用等。这里的类型速度从慢到快,扫描的行数从多到少。
如果发现查询需要扫描大量的数据但只返回少量的行,可以通过以下方式进行优化:
在传统实现中,总是强调需要数据库完成尽可能多的工作。但是对于MySQL而言,这并不适用,MySQL从设计上让连接和断开连接都非常轻量级,在返回一个小的查询结果方面很高效。
如果将一个复杂的查询拆分成多个简单查询对应用更友好,能减少更多的工作,就不要害怕这样做。
我们可以将一个大的查询“分而治之”,将大查询分成小查询,每个小查询 功能相同,只完成一小部分,每次只返回一小部分查询结果。这样可以减轻服务器负担,避免一次性锁住很多数据,占用过多事务日志等。
如果是事务性引擎,很多时候小事务可能更高效
很多高性能的应用都会对关联查询进行分解:可以对每一个表进行一次单标查询,然后将结果在应用程序中进行关联。如:
select
e.emp_no ,
e.first_name ,
e.last_name ,
d.dept_name
from
employees e
inner join dept_emp ed
using (emp_no)
inner join departments d
using(dept_no)
where
e.first_name = 'Mary'
可以分解为:
select e.emp_no,e.first_name,e.last_name from employees e where e.first_name = 'Mary';
select d.dept_no from dept_emp de where de.emp_no = 10011;
select d.ept_name from departments d where d.dept_no = 'd009';
分解关联查询的方式重构查询有如下优势:
在如下场景中,将关联放在应用程序中将会更加高效:
IN()
的方式代替关联查询的时候;如果我们弄清楚MySQL是如何优化和执行查询的,我们就可以理解:很多查询优化工作实际上就是遵循一些原则让优化器能够按照预想的合理的方式运行。
MySQL查询执行过程:
MySQL客户端/服务端通信协议是 半双工 的,这种协议让MySQL通信变得简单,但是也从很多地方限制了MySQL。一个明显的限制就是无法进行 流量控制 ,一单一端开始发生消息,另一端要接收完整的消息之后才能响应它。
客户端用 一个 单独的数据包将查询传给服务器。这也是为什么当查询语句很长时,参数max_allowd_packet
就特别重要了。
相反,服务器响应给于鸿鹄的数据通常较多,由 多个 数据包组成。客户端必须完整的接受整个返回结果,所以查询中使用LIMIT
限制是十分有意义的。
MySQL通常需要等所有的数据都已经发送给客户端才能释放这条查询所占用的资源。所以缓存通常可以减少服务器压力,让查询早点结束、早点释放响应的资源。
对于一个MySQL连接,或者说一个线程任何时候都有一个状态,该状态表示了MySQL当前正在做什么。我们可以查询通过SHOW FULL PROCESSLIST
命令结果中的Command列来查看连接的当前状态:
show full processlist;
Id | User | Host | db | Command | Time | State | Info |
---|---|---|---|---|---|---|---|
4 | event_scheduler | localhost | Daemon | 172374 | Waiting on empty queue | ||
273 | develop | 192.168.1.4:56494 | employees | Sleep | 11306 | ||
274 | develop | 192.168.1.4:56495 | employees | Sleep | 7230 | ||
275 | develop | 192.168.1.4:56496 | employees | Query | 0 | starting | /* ApplicationName=DBeaver 7.1.0 - SQLEditor |
GROUP BY
操作,要么是文件排序操作,或者是UNION
操作。如果查询缓存是打开的,那么在解析一个查询语句之前,MySQL会优先检查这个查询是否命中查询缓存中的数据。检查是通过一个大小写敏感的哈希查找实现的。如果当前缓存命中了查询缓存,那么在返回查询结果之前MySQL会检查一次用户权限。如果权限没有问题,将直接从缓存中拿到结果并返回给客户端。
首先,MySQL通过 关键字 将SQL语句进行解析并生成一颗对应的“解析树”。MySQL解析器将使用MySQL语法规则 验证和解析查询。例如关键字是否正确,关键字的顺序是否正确等。————语法合法性
然后,预处理器则根据一些MySQL规则进一步检查解析树是否合法。例如,检查数据表和数据列是否存在,表别名是否有歧义等。————表结构合法性
然后,预处理器还要验证权限。
优化器将合法的语法树转化为 执行计划。一条查询有多种执行计划,优化器的作用就是找出最好的执行计划。
查询优化器能够优化的一些类型:
当外连接与内连接等价时
COUNT()
,MIN()
和MAX()
MIN():B-Tree索引第一个节点
MAX():B-Tree索引最以后一个节点
没有Where条件的COUNT():使用存储引擎提供的一些优化(存储表行数的变量)
当一个表达式(甚至是一个查询)可以转化为一个常数时
覆盖索引扫描
子查询优化
减少多个查询多次对数据进行访问
当知道已经满足查询需求时,MySQL总是能够立即终止查询。发现一个不成立条件就立即结束。
如果两个列通过等式关联,那么MySQL能够吧其中一个列的WHERE条件传递给另一个列
select
e.emp_no ,
e.first_name ,
e.last_name ,
d.dept_name
from
employees e
inner join dept_emp ed
using (emp_no)
inner join departments d
using(dept_no)
where
e.emp_no = 10001;
将会优化为:
select
e.emp_no ,
e.first_name ,
e.last_name ,
d.dept_name
from
employees e
inner join dept_emp ed
using (emp_no)
inner join departments d
using(dept_no)
where
e.emp_no = 10001
and ed.emp_no = 10001;
MySQL将IN()列表中的数据先排序,然后通过二分查找的方式来确定列表中的值是否满足条件。
MySQL认为任何一次查询都是一次“关联”,MySQL的关联执行策略是 嵌套循环关联操作。
MySQL生成查询的指令树,然后通过存储引擎执行完成这可树的指令并返回结果。
我们对某个查询执行explain
后,再执行SHOW WARNINGS
,就可以看到重构出的查询。
通常情况下,查询优化器会选择最佳(嵌套循环最少)的表关联顺序进行查询,但是当关联的表十分多的时候,优化器就会使用“贪婪”搜索方式查找“最优”的关联顺序。
实际上,当需要关联的表超过optimizer_serch_depth
的限制,就会选择“贪婪”索索模式了。
我们也可以在查询语句中使用STRAIGHT_JOIN
关键字,使得优化器不优化表关联顺序。
select STRAIGHT_JOIN
e.emp_no ,
e.first_name ,
e.last_name ,
d.dept_name
from
employees e
inner join dept_emp ed
using (emp_no)
inner join departments d
using(dept_no)
where
e.emp_no = 10001
and ed.emp_no = 10001;
当MySQL不能通过索引得到排序结果时,就需要自己进行排序,如果数据量小就在内存中进行,如果数据量大则需要使用硬盘,MySQL将这个过程称为 文件排序(filesort)。
如果需要排序的数据量小于“排序缓冲区”,MySQL使用内存进行“快速排序”操作。如果内存不够排序,那么MySQL会先将数据分块,对每个独立的块使用“快速排序”进行排序,并将各个快的排序结果放在磁盘上,然后将各个排好序的块进行合并,最后返回排序结果。
MySQL有如下两种排序算法:
读取行指针和需要排序的字段,对其进行排序,然后在根据排序结果读取所需数据行。该算法在第二次读取数据时会造成大量的随机I/O,所以两次传输排序的数据传输成本非常高。但是这个算法在排序时存储尽可能少的数据,是的“排序缓冲区”中能够容纳更多的行数进行排序。
单次传输排序(新版本使用)
先读取查询所需的所有列,然后在根据给定列进行排序,最后直接返回排序结果。该算法的优缺点与两次传输相反。
当查询需要所有列的总长度不超过max_length_sort_data
时,MySQL会使用“单次传输排序”,我们可以通过调整该参数来影响MySQL排序算法的选择。
在关联查询的时候如果需要排序:
如果ORDER BY子句的所有列都来自关联的第一个表,那么MySQL在关联处理第一个表的时候就会进行文件排序。这种情况下,EXPLAIN 的Extra字段可以看到"Using filesort"。
除去第一种情况的其它情况,MySQL都会先将关联的记过存放在一个临时表总,然后再所有的关联结束后,再进行文件排序。XPLAIN 的Extra字段可以看到"Using temporary;Using filesort"。
如果有LIMIT,LIMIT也会在排序之后应用,所以即使需要返回较少的数据,临时表和需要排序的数据量任然会非常大。
MySQL的查询执行引擎根据执行计划给出的指令逐步执行,在执行计划逐步执行的过程中,大量的操作需要通过调用存储引擎实现的接口来完成。
查询执行的最后一个阶段是将结果返回给客户端。如果查询可以别缓存,那么查询结果也在这个阶段被存放到查询缓存中。
MySQL将结果返回给客户端是一个增量、逐步返回的过程。如关联查询,一旦查询处理完最后一个关联表,开始生成第一条结果是,MySQL就可以开始向客户端逐步返回结果集了。这样做有两个好处:
结果集中的每一行都会以满足MySQL客户端/服务器通信协议的封包发送,再通过TCP协议进行传输。
统计列值时要求列值非空(列值为NULL的记录不被统计在内),统计行数就是统计结果集的行数,不会关注列值。
MyISAM的COUNT(*)
函数在没有where条件时非常的快,因为它利用存储引擎的特性直接获取这个值。我们可以利用这个特性,来加速一些特定条件下的COUNT()
查询:
如果一个统计需要扫描多半张表,那么我们可以将条件反转一下,统计其对立面,然后在用总数减去对立面数量:
select count(*) from employees e2 where e2.emp_no > 10005;
show status where variable_name = 'Handler_read_next ';
该统计语句需要扫描30023行数据。
select ((select count(*) from employees) - count(*) ) total from employees e2 where e2.emp_no <= 10005;
show status where variable_name = 'Handler_read_next ';
这条查询只扫描了5行。因为子查询直接被当做常数来处理的:
id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | PRIMARY | e2 | range | PRIMARY | PRIMARY | 4 | 5 | 100.0 | Using where; Using index | ||
2 | SUBQUERY | Select tables optimized away |
select sum(if (first_name like 'A%',1,0)) as Aname,sum(if (first_name like 'B%',1,0)) as bname from employees_temp et ;
--或者
select sum(first_name like 'A%') as Aname,sum(first_name like 'B%') as bname from employees_temp et ;
--或者使用count(),只要将其条件设置为真
select count(first_name like 'A%' or null) as Aname,count(first_name like 'B%' or null) as Bname from et ;
如果某些业务场景不要求完全精确的COUNT值,此时可以用近似值来代替。EXPLAIN出来的优化器估算的行数就是一个不错的近似值,执行EXPLAIN并不需要真正地去执行查询,所以成本很低。
如果及时是索引覆盖扫描都不够的话,那么就需要考虑汇总表或者外部缓存系统了。
在MySQL5.6之前,尽可能的使用关联查询,5.6及以后可以直接忽略这条建议。
当偏移量过大时,MySQL需要查询大量数据然后丢弃,十分影响性能,我们可以使用“延迟关联”来减少读取行数。
select emp_no ,first_name ,last_name from employees e limit 10000,20;
--
select emp_no ,first_name ,last_name from employees e1 join (select emp_no from employees e2 limit 10000,20 ) e3 using(emp_no);
我们经常需要将WHERE、LIMIT、ORDER BY等子句“下推”到UNION的各个子查询中,以便优化器可以充分的利用这些条件进行优化。
除非确实需要服务器消除重复的行,否则一定要使用UNION ALL.