在传统实现中,总是强调需要数据库层完成尽可能多的工作,这样做的逻辑在于以前总是认为网络通信、查询解析和优化是一件代价很高的事情。但是这样想法对于MySQL并不适用,MySQL从设计上让链接和断开都很轻量级,返回一个小查询结果反面很高效。现代网络比以前快很多,无论是贷款还是延迟,在一些版本上,每秒千兆网络也能轻松满足美妙2000次的查询。
有时候对一个大查询我们需要“分而治之”,将大查询切分成小查询,每个查询功能完全一样,只完成小部分,每次只返回一小部分查询结果。比如删除旧数据时候,加上一个limit 10000,循环删除。可以降低对表的锁时间,中间如果暂停一会儿,可以将服务器上原本一次性的压力分解到一个很长的时间段中,降低服务器的影响。
很多高性能的应用都会对关联查询进行分解,简单的对每一个表进行一次表单查询,然后将结果在应用程序中进行关联。
例如:select * fromtag join a on ….join b on …where
可以分解为:select *from tag where …
Select * from a where …
Select * fro b in ()…
这样做的优势在于
1、 让缓存的效率更高,许多应用程序可以方便缓存单表查询对应的结果对象。
2、 将查询分解后,执行单个查询可以减少锁的竞争。
3、 在应用层做关联,可以更容易对数据库进行拆分,更容易做到高性能和可扩展。
4、 查询本身的效率也会有所提升,用IN()代替关联查询,可以让MySQL按照ID顺序进行查询,这可能比随机关联的要更高效。
5、 可以减少冗余记录的查询,在应用层做关联查询,这意味着对于某条记录应用只需要查询一次,而在数据库中做关联查询,则可能需要重复的访问一部分数据。从这点看,这样的重构还可能会减少网络和内存的消耗。
6、 更进一步,这样做相当于在应用中做了哈希关联,而不是使用MySQL嵌套循环关联,某些场景哈希关联的效率要高很多。在很多场景下,通过重构查询将关联到应用程序中将会更高效。
MySQL执行一个查询的过程,步骤如下:
1、 客户端发送一条查询给服务器
2、 服务器先检查缓存、如果查询命中了缓存,则立刻返回存储在缓存中的结果。否则进入下阶段。
3、 服务器端进行SQL解析、预处理,再由优化器生成对应的执行计划。
4、 MySQL根据优化器生成的执行计划,调用存储引擎的API来执行查询
5、 将结果返回给客户端
MySQL客户端和服务器端通信是半双工的,这意味着,在任何时刻,要么是服务器向客户端发送数据,要么是由客户端向服务器发送数据。所以我们无法也无需将一个消息切成小块独立来发送。
服务器响应给用户的数据有很多,当服务器开始响应客户端请求时,客户端必须完整地接收结果,然后取前面几条需要的结果,所以在必要时候在查询加上LIMIT限制的原因。
对于一个MySQL连接,或者说一个线程,任何时刻都有一个状态,该状态表示了MySQL当前在做什么,通过show Fullprocesslist查看,在一个查询的生命周期中,状态会变化很多次,以下是状态列:
Sleep 线程正在等待客户端发送新的请求
Query 线程正在执行查询或者正在将结果发送给客户端
Locked 在MySQL服务层,该线程正在等待表锁。在存储引擎级别实现的锁,例如INNODB的行锁,并不会体现在线程状态中。对于myisam 来说这是比较典型的状态。
Analyzing and statistics线程正在收集存储引擎的统计信息,并生成查询的执行计划。
Copying to tmptable 线程正在收集存储引擎的统计信息,并生成查询的执行计划。
Sorting result 线程正在对结果集进行排序
Sending data 线程可能在多个状态之间传送数据,或者在生成结果集,或者在向客户端返回数据。
通过判断状态来查看那个占用了大量的时间。
在解析一个查询语句之前,如果缓存是打开的,那么MySQL会优先检查这个查询是否命中查询缓存中的数据。这个检查是通过一个对大小写敏感的哈希查找来实现的。查询和缓存中的值如果有一个字节不同,那么也不会匹配缓存结果。
查询的生命周期的下一步是将一个SQL转换成一个执行计划,MySQL再依照这个执行计划和存储引擎进行交互。包括:解析SQL、预处理、优化SQL执行计划。
语法解析器和预处理
解析语法规则验证、解析查询、关键字顺序是否正确、引号是否匹配、查询表和数据列是否存在、解析名字、别名。
查询优化器
MySQL使用基于成本的优化器,它将尝试预测一个查询使用某种执行计划时的成本,并选择其中成本最小的一个。最初,成本最小的单位是随机读取一个4K数据页的成本,后来引入一些因子来计算操作的代价。一般是通过:每个表或者索引的页面个数、索引的基数、索引、数据行长度、索引分布情况。优化器在评估成本的时候并不考虑任何层面缓存,它假设读取任何数据都需要一次磁盘I/O。
优化策略可以简单分为两种,一种是静态优化,一种是动态优化。静态优化可以直接对解析树进行分析,并完成优化。例如:优化器可以通过一些简单的代数变换将where条件转换成另外一种等价形式。静态优化不依赖特别的数值,在第一次完成后就一直有效,类似编译时优化。
动态优化和查询上下文有关,例如where条件中的取值、索引中条目对应的数据行,这个要在每次查询的时候都重新评估。
下面是MySQL能够处理的优化类型:
重新定义关联表的顺序
数据表的关联并不总是按照在查询中指定的顺序进行。决定关联的顺序是优化器很重要的一部分功能。
将外连接转化成内连接
OUTER JOIN 的语句中,像where条件、库表结构都可能会让外连接等价一个内连接。
使用等价变换规则
MySQL可以使用一些等价变换来简化并规范表达式,例如5=5 and a>5 将被改写成a>5.(a5 and b=c and a=5
优化count(),min(),max()
当索引和列是否可为空通常可以帮助MySQL优化这类表达式,例如找打某一列的最小值,只需要查询对应B-Tree索引最左端的记录,MySQL可以直接获取索引的第一条,最大的也是获取索引的最后一条。
预估并转化为常数表达式
当MySQL检测到一个表达式可以转化为常数的时候,就会一直把该表达式作为常数进行处理,如果where子句中使用了该类索引的常数条件,MySQL可以在查询开始阶段就先查到这些值,当一个id字段有主键索引,MySQL优化器知道这只会返回一行数据,这里的表访问类型是const。
覆盖索引扫描
当索引中的列包含所有查询中需要使用的列的时候,MySQL就可以使用索引返回需要的数据,而无需查询对应的数据行
子查询优化
MySQL在某些情况下可以将子查询转换成一种效率更高的形式,从而减少多个查询多次多数据进行访问
提前终止查询
在发现已经满足查询要求时,MySQL总是能够提前终止查询。
等值传播
如果两个列的值通过等式关联,那么MySQL能够把其中一个列的where条件传递到另一个列上,例如:select film.film_id fromsakila.film inner join sakila.film_actor using(film_Id) where film.film_id>500;
MySQL可以知道film_id在film_actor 和film两个表都适用
列表IN()比较
MySQL中,in()列表中的数据线进行排序,然后通过二分法查找的方式来确定列表中的值是否满足条件,这是一个O(log n)复杂度的操作,等价转换成OR查询的复杂度为O(N),对于IN()列表中有大量取值的时候,MySQL的处理速度更快。
在服务器层有查询优化器,没有保存数据和索引的统计信息。统计信息由存储引擎实现,不同的存储引擎可能会存储不同的统计信息。
MySQL查询优化器在生成查询的执行计划时,需要向存储引擎获取相应的统计信息。存储引擎则提供给优化器对应的统计信息,包括:每个表后者索引有多少个页面,每个表的每个索引基数是多少,数据行和索引长度、索引的分布信息等,优化器根据这些信息来选择一个最优的执行计划。
MySQL的关联不仅仅只限于两张表联合查询,MySQL认为每次的查询都可能是一次关联,我们看看UNION查询,MySQL先将一系列的单个查询结果放到一个临时表中,然后重新读取临时表数据完成UNION查询,在MySQL概念中,每次查询都是一个关联,所以读取临时表也是一次关联。
MySQL对于任何关联都执行嵌套循环关联操作,即MySQL先在一个表中循环取出单条数据,然后嵌套循环到下一个表中寻找匹配的行,依次下去直到找到所有表中匹配的行为止,然后根据各个表匹配的行,返回查询中需要的各个列。MySQL会尝试在最后一个关联表中找到所有匹配的行,如果最后一个关联表无法找到更多的行以后,MySQL返回到上层关联表,看能否找到更多的匹配记录,依次类推迭代执行。例如如下的例子,介绍什么是“嵌套循环关联”:
Selecttbl1.col1,tbl2.col2 from tbl1 inner join tbl2 using(col3) where tbl1.col1in(5,6);
Outer_iter =iterator over tbl1 where col1 in(5,6)
Outer_row =outer_iter.next
While outer_row
Inner_iter = iterator over tbl2 where col3 =outer_row.col3
Inner_row = inner_iter.next
End
Outer_row =outer_iter.next
End
MySQL在from子句中遇到子查询时,先执行子查询并将结果放到一个临时表中,然后将这个临时表当做一个普通表对待。
执行计划
MySQL生成查询的一棵指令树,然后通过存储引擎执行完成这棵树指令树并返回结果。最终的执行计划包含了重构查询的全部信息。MySQL执行查询方式是从一个表开始一直嵌套循环,回溯完成所有的表关联,是一棵左侧深度优先的树。
关联查询优化器
MySQL优化器最重要的一部分就是关联查询优化,它决定了多个表关联时的顺序。通常多表关联时候,可以有多种不同的关联顺序来获得相同的执行结果,关联优化器则通过评估不同顺序时的成本来选择一个代价最小的关联顺序。例如当有多个inner join …inner join时,mysql会优化关联顺序,重新定义关联的顺序来达到更少的嵌套循环和回溯操作。
排序优化
当不能使用索引排序的时候,MySQL需要自己进行排序,如果数据量小则在内存中进行,如果数据量大则需要使用磁盘,统称为文件排序,即使完全是内存排序不需要任何磁盘文件也是如此。
MySQL有两种排序算法:
两次传输排序
读取行指针和需要排序的子弹,对其进行排序,然后根据排序结果读取所需要的数据行。
单次传输排序
先读取所需要的所有列,然后再根据给定列进行排序,最后直接返回排序结果。这种不需要读取两次数据,在对于I/O密集型的应用,这样的效率提高了很多,这种算法只需要一次顺序I/O读取所有的数据,而无需任何的随机I/O。
在关联查询的时候,如果需要排序,MySQL会分两种情况来处理这样的文件排序,如果order by 子句中的所有列都来自关联的第一个表,那么MySQL在关联处理第一个表就进行了文件排序。如果查询中有,在explain 中的 extra字段会有 “Using filesort”,如果查询中有LIMIT的话,LIMIT也会在排序之后应用,所以即使需要返回较少的数据,临时表和需要排序的数量仍然会非常大。
在解析和优化阶段,MySQL将生成查询对应的执行计划,MySQL的查询执行引擎根据这个执行计划来完成整个查询,这里的执行计划只是一个数据结构,不会生成对应的字节码.
MySQL将结果返回客户端是一个增量/逐步返回的过程,例如当服务器处理完最后一个关联表,生成第一条结果时,MySQL就可以像客户端逐步返回结果集了。
这样的好处在于:服务器端无需存储太多的结果,也就不会因为要返回太多的结果而消耗太多的内存。另外这样的处理也让MySQL客户端第一时间获得返回的结果。