MySQL优化查询性能的常用方法

本文为《高性能MySQL(第三版)》一书的摘要总结

优化数据访问

如果要优化查询,实际上是要优化其子任务,要么要出其中一些子任务,要么减少子任务的执行次数,要么让子任务运行的更快。

查询性能低下最基本的原因是访问的数据太多。对于低效的查询,下面两个步骤的分析总是很有效:

  • 确认应用程序是否在检索大量超过需要的数据。这意味着访问了太多行后者列。
  • 确认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是否在扫描额外的数据

在确定查询只返回需要的数据 之后,接下来应该看看查询为了返回这些结果是否扫描了过多的数据。

对于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';

分解关联查询的方式重构查询有如下优势:

  • 让缓存的效率更高。不管是应用程序缓存还是MySQL查询缓存,对单表查询结果进行缓存,都将提高缓存的利用率。
  • 将查询分解后,执行单个查询可以减少锁的竞争。
  • 在应用层做关联,可以更容易对数据库进行拆分,更容易做到高性能和可扩展。
  • 查询本身效率也可能会有所提升。(单表查询更容易实现顺序查询,这可能比随机的关联更高效)
  • 可减少冗余记录的查询。在应用层做关联,意味着对于某条记录应用只需要查询一次;而在关联查询中,可能需要重复的查询某一部分数据。
  • 这样做相当于在应用中实现了 哈希关联

在如下场景中,将关联放在应用程序中将会更加高效:

  • 当应用能够方便的缓存单个查询结果时;
  • 当可以将数据分布到不同的MySQL服务器上时;
  • 当能够用IN()的方式代替关联查询的时候;
  • 当查询中使用同一个数据表的时候。

查询执行的基础

如果我们弄清楚MySQL是如何优化和执行查询的,我们就可以理解:很多查询优化工作实际上就是遵循一些原则让优化器能够按照预想的合理的方式运行。

MySQL查询执行过程:

MySQL优化查询性能的常用方法_第1张图片

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 */ show processlist
  • Sleep: 线程正在等待客户端发送新请求。
  • Query:线程正在执行查询或者正在将结果发送给客户端
  • Locked:在MySQL服务器层,该线程正在等待 表锁
  • Analyzing and statistics:线程正在搜集存储引擎的统计信息,并生成查询的执行计划
  • Copying to tmp table [on disk]: 线程正在执行查询,并且将其结果集都复制到一个零时表中,这种状态一般要么是在做GROUP BY操作,要么是文件排序操作,或者是UNION操作。
  • Sorting result:线程正在对结果集进行排序。
  • Sending data:线程可能在多个状态之间传送数据,或者在生成结果集,或者在向客户端返回数据。

查询缓存(Query Cache)

如果查询缓存是打开的,那么在解析一个查询语句之前,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;
  • 列表IN()的比较

MySQL将IN()列表中的数据先排序,然后通过二分查找的方式来确定列表中的值是否满足条件。

MySQL关联查询

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协议进行传输。

优化特定查询类型

优化COUNT()查询

统计列值时要求列值非空(列值为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并不需要真正地去执行查询,所以成本很低。

  • 更复杂的优化

如果及时是索引覆盖扫描都不够的话,那么就需要考虑汇总表或者外部缓存系统了。

优化关联查询

  • 确保On或者Using子句中的列上有索引,且是第二个表的列上有索引。
  • 确保任何GOURP BY和ORDER BY中的表达式只涉及到一个表中的列。这样MySQL才有可能使用索引来优化这个过程。

优化子查询

在MySQL5.6之前,尽可能的使用关联查询,5.6及以后可以直接忽略这条建议。

优化LIMIT分页

当偏移量过大时,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);

优化UNION

我们经常需要将WHERE、LIMIT、ORDER BY等子句“下推”到UNION的各个子查询中,以便优化器可以充分的利用这些条件进行优化。

除非确实需要服务器消除重复的行,否则一定要使用UNION ALL.

你可能感兴趣的:(MySQL)