查询性能优化

为什么查询速度会慢

在尝试编写快速的查询之前,需要清楚的一点就是,真正重要的是响应时间。如果把查询看作是一个任务,那么它由一系列子任务组成,每个字任务都会消耗一定的时间。如果要优化查询,实际上要优化其子任务,要么消除其中一些子任务,要么减少子任务的执行次数,要么让子任务运行的更快。

通常来说,查询的生命周期大致可按照顺序来看:从客户端,到服务器,然后在服务器上进行解析,生成执行计划,执行,并返回结果给客户端。其中“执行”可以认为是整个生命周期中最重要的阶段,这其中包括了大量为了检索数据到存储引擎的调用以及调用后的数据处理,包括排序、分组等。 对于一个查询的全部生命周期,上面列的并不完整,这里只是想说明,了解查询的生命周期,清楚查询的时间消耗情况对于优化查询有很大的意义。

慢查询基础:优化数据访问

查询性能低下最基本的原因是访问的数据太多,大部分性能低下的查询都可以通过减少访问的数据量的方式进行优化。对于低效的查询,通过下面两个步骤来分析总是很有效的:

  • 确认应用程序是否在检索大量超过需要的数据。这通常意味着访问了太多的行,但有时候也有可能是访问了太多的列。
  • 确认MySQL服务器是否在分析大量超过需要的数据行。

是否向数据库请求了不需要的数据

有些查询会请求超过实际需要的数据,然后这些多余的数据会被应用程序丢弃,这会给MySQL服务器带来额外的负担,并增加网络开销,另外也会消耗应用服务器的CPU和内存资源。下面是一些典型的案例:

  • 查询不需要的记录:一个常见的错误是常常会误以为MySQL会只返回需要的数据,实际上MySQL确实先返回全部结果集再进行计算。最简单有效的解决方法就是在这样的查询后面加上LIMIT。
  • 多表关联时返回全部列:例如你想查询所有在电影Academy Dinosaur中出现的演员,千万不要下面这种写法编写查询:
    1
    2
    3
    4
    SELECT * FROM actor
    INNER JOIN film_actor USING(actor_id)
    INNER JOIN film USING(film_id)
    WHERE fime.title = 'Academy Dinosaur';

这将返回这三个表的全部数据列。正确的方式应该是像下面这样只取需要的列:SELECT actor.* FROM actor...

  • 总是取出全部列:每次看到SELECT * 的时候都需要用怀疑的眼光审视,是不是真的需要返回全部的列?这无疑会增加额外的消耗。但是如果这种有点浪费数据库资源的方式可以简化开发,因为提高相同代码片段的复用性,如果清楚这样做的性能影响,那么这种做法也是值得考虑的。如果应用程序使用了某种缓存机制也可以,获取并缓存所有的列的查询,相比多个独立的只获取部分列的查询可能就更有好处。
  • 重复查询相同的数据:如果不断地重复执行相同的查询,然后每次都返回相同的数据,这样是完全没有必要的。比较好的方案是,当初次查询的时候将这个数据缓存起来,需要的时候从缓存中取出,这样的性能显然会更好。

MySQL是否在扫描额外的记录

在确定查询只返回需要的数据以后,接下来该看看查询为了返回结果是否扫描了过多的数据。对于MySQL,最简单的衡量查询开销的三个指标如下:

  • 响应时间
  • 扫描的行数
  • 返回的行数

响应时间

响应时间只是一个表面上的值,但是响应时间仍然是最重要的指标。响应时间是两个部分之和:服务时间排队时间服务时间是指数据库处理这个查询真正花了多长时间;排队时间是指服务器因为等待某些资源而没有真正执行查询的时间,一般最常见和重要的是I/O等待和锁等待。

当你看到一个查询的响应时间的时候,首先需要问问自己,这个响应时间是否是一个合理的值。实际上可以使用“快速上限估计”法来估算查询的响应时间,概括地说,了解这个查询需要哪些索引以及它的执行计划是什么,然后计算大概需要多少个顺序和随机I/O,再用其乘以在具体硬件条件下一次I/O的消耗时间,最后把这些消耗都加起来,就可以获得一个大概参考值来判断当前响应时间是不是一个合理的值。

扫描的行数和返回的行数

分析查询时,查看该查询扫描的行数是非常有效的,这在一定程度上能说明该查询找到需要的数据和效率高不高。并不是所有的行的访问代价都是相同的,较短的行的访问速度更快,内存中的行比磁盘中的行的访问速度要快得多。

理想情况下扫描的行数和返回的行数应该是相同的,但实际中这种情况并不多,扫描的行数对返回的行数的比率通常很小,一般在1:1和10:1之间,不过有时候这个值也可能非常大。

扫描的行数和访问类型

在评估查询开销的时候,需要考虑一下从表中找到某一行数据的成本。MySQL的好几种访问类型,有些方式可能需要扫描很多行才能返回一行,也有些方式可能无需扫描就能返回结果。

在EXPLAIN语句中的type列反应了访问类型,访问类型有很多种,从全表扫描到索引扫描、范围扫描、唯一索引查询、常数索引等,这里列的这些,速度是从慢到快,扫描的行数也是从多到少的。

如果查询没有办法找到合适的访问类型,那么解决的最好办法通常就是增加一个合适的索引,索引让MySQL以最高效、扫描行数最少的访问找到需要的记录。

一般MySQL能够使用如下三种方式应用WHERE条件,从好到坏依次为

  • 在索引中使用WHERE条件来过滤不匹配的记录。这是在存储引擎层完成的。
  • 使用索引覆盖扫描(在Extra中出现了Using index)来返回记录,直接从索引中过滤不需要的记录并返回命中的结果。这是在MySQL服务层完成的,但无须再回表查询记录。
  • 从数据表中返回数据,然后过滤不满足条件的记录(在Extra列中出现Using Where)。这在MySQL服务器层完成,MySQL需要先从数据表读出记录然后过滤。

好的索引很重要,可以让查询使用合适的访问类型,尽可能的只扫描需要的数据行,但也不是说增加索引就能让扫描的行数等于返回的行数。如果发现查询需要扫描大量的数据但只返回少数的行,那么通常可以尝试下面的技巧去优化:

  • 使用索引覆盖扫描,把所有需要用的列都放到索引中,这样存储引擎就无须回表获取对应行就可以返回结果了。
  • 改变库表结构,例如使用单独的汇总表。
  • 重写这个复杂的查询,让MySQL优化器能以更优化的方式执行这个查询。

重构查询的方式

在优化有问题的查询时,目标应该是找到一个更优的方法获得实际需要的结果,而不一定总是需要从MySQL获取一模一样的结果集,有时可以将查询转换一种写法让其返回一样的结果,但是性能更好。

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

在传统实现中,总是强调需要数据库层完成尽可能多的工作,这样做的逻辑在于以前总是认为网络通信、查询解析和优化时是一件代价很高的事情。但是这样的想法对于MySQL并不适用,MySQL从设计上让连接和断开连接都很轻量级,在返回一个小的查询结果方面很高效。

MySQL内部每秒能扫描内存中上百万行数据,相比之下,MySQL响应数据给客户端就慢得多了。在其他条件都相同的时候,使用尽可能少的查询当然是最好的。但是有时候将一个大查询分解为多个小查询是很有必要的。

切分查询

有时候对于一个大查询我们需要“分而治之”,将大查询切分成小查询,每个查询功能完全一样,只完成一小部分,每次只返回一小部分查询结果。

例如在删除旧的数据时,定期清除大量数据时,如果用一个大的语句一次性完成的话,则可能需要一次锁住很多数据,占满整个事务日志,耗尽系统资源、阻塞很多小的但是重要的查询。将一个大的DELETE语句切分成多个较小的查询可以尽可能小的影响MySQL性能,同时还可以减少MySQL复制的延迟。例如我们可以每次只删除一部分数据,每次删除数据后都暂停一会在做下一次删除,这样也可以将服务器上原本一次性的压力分散到一个很长的时间段中,就可以大大降低对服务器的影响,还可以大大减少删除时锁的持有时间。

分解关联查询

很多高性能的应用都会对关联查询进行分解,简单地,可以对每一个表进行一次单表查询,然后将结果在应用程序中进行关联。例如下面这个查询:

1
2
3
4
SELECT * FROM tag 
JOIN tag_post ON tag_post.tag_id = tag.id
JOIN post ON tag_post.post_id = post.id
WHERE tag.tag = 'mysql';

可以分解成下面这些语句来代替:

1
2
3
SELECT * FROM tag WHERE tag = 'mysql';
SELECT * FROM tag_post WHERE tag_id = 1234;
SELECT * FROM post WHERE post.id in(123,456,567,9098,8904);

用分解关联查询有如下优势:

  • 让缓存的效率更高。许多应用程序可以方便地缓存单表查询对应的结果对象,对MySQL的查询缓存来说,如果关联中的某个表发生了变化,那么就无法使用查询缓存了,而拆分后,如果某个表很少改变,那么基于该表的查询就可以重复利用查询缓存结果了。
  • 将查询分解后,执行单个查询可以减少锁的竞争。
  • 在应用层做关联,可以更容易对数据库进行拆分,更容易做到高性能和可扩展。
  • 查询本身效率也可能会有所提升,例如用IN()代替关联查询,可以让MySQL按照ID顺序进行查询,这可能比随机的关联更高效。
  • 可以减少冗余记录的查询。在应用层做关联查询,意味着对于某条记录应用只需要查询一次,而在数据库中做关联查询,则可能需要重复地访问一部分数据,所以这样的重构还可能会减少网络和内存的消耗。
  • 更进一步,这样相对于在应用中实现了哈希关联,而不是使用MySQL的嵌套循环关联。

在很多场景下,通过重构查询将关联放在应用程序中将会更加高效,比如:当应用能够方便的缓存单个查询的结果的时候、当可以将数据分不到不同的MySQL服务器上的时候、当能够使用IN()的方式代替关联查询的时候、当查询中使用同一个数据表的时候。

查询执行的基础

下图可以看出当向MySQL发送一个请求的时候,MySQL到底做了些什么:

  1. 客户端先发送一条查询给服务器。
  2. 服务器先检查缓存,如果命中缓存,则立刻返回存储在缓存中的结果。否则进入下一阶段。
  3. 服务端进行SQL解析、预处理,再由优化器生成对应的执行计划。
  4. MySQL根据优化器生成的执行计划,调用存储引擎的API来执行查询。
  5. 将结果返回给客户端。

MySQL客户端/服务器通信协议

MySQL客户端和服务器之间的通信协议是半双工的,这意味着,在任何一个时刻,要么是由服务器向客户端发送数据,要么是由客户端向服务器发送数据,这两个动作不能同时发生。所以,无法也无需将一个消息切成小块独立来发送。这种协议虽然让MySQL通信简单快速,但是一个明显的限制就是没法进行流量控制,一旦一端开始发送消息,另一端要接收完整个消息才能响应它。即一旦客户端发送了请求,它能做的事情就只是等待结果了。

相反的,一般服务器相应给客户的数据很多,由多个数据包组成。当服务器开始响应客户端请求时,客户端必须完整地接收整个返回结果,而不能简单地只取几条结果。这也是在必要时侯一定要在查询中加上LIMIT限制的原因。

多数连接MySQL的库函数都可以获得全部结果集并缓存到内存里,还可以逐行获取需要的数据。默认一般是获得全部结果集并缓存到内存中。MySQL通常需要等所有的数据都已经发送给客户端才能释放这条查询所占用的资源,所以接受全部结果并缓存通常可以减少服务器的压力,早点释放相应的资源。

查询状态

对于一个MySQL连接,或者说一个线程,任何一个时刻都有一个状态,该状态标识了MySQL当前正在做什么。最简单的查看方式是使用SHOW FULL PROCESSLIST命令,在一个查询生命周期中的状态有这么几种:

  • Sleep:线程正在等待客户端发送新的请求。
  • Query:线程正在执行查询或者正在将结果发送给客户端。
  • Locked:在MySQL服务层,该线程正在等待表锁。在存储引擎级别实现的锁,例如InnoDB的行锁并不会体现在线程装态中。对于MyISAM来说这是一个比较典型的状态,但在其他没有行锁的引擎中也会经常出现。
  • Analyzing and statistics:线程正在收集存储引擎的统计信息,并生成查询的执行计划。
  • Copying to tmp table [on disk]:线程正在执行查询,并且将其结果集都复制到一个临时表中,这种状态一般要么是在做GROUP BY操作,要么是文件排序操作,或者UNION操作。如果这个状态后面还有“on disk”标记,那么表示MySQL正在将一个内存临时表放在磁盘上。
  • Sorting result:线程正在对结果集进行排序。
  • Sending data:这种情况下线程可能在多个状态之间传送数据,或者在生成结果集,或者在向客户端返回数据。

查询缓存

在解析一个查询语句之前,如果查询缓存是打开的,那么MySQL会优先检查这个查询是否命中查询缓存的数据。这个查询时通过一个对大小写敏感的哈希查找实现的。

如果当前查询恰好命中查询缓存,那么在返回查询结果之前MySQL会检查一次用户权限。这仍然是无须解析查询SQL语句的。如果没有权限问题,MySQL会直接从缓存中拿到结果并返回给客户端。这总情况下,查询不会解析,不用生成执行计划,不会被执行。

查询优化处理

查询的生命周期的下一步是将一个SQL转换成一个执行计划,MySQL再按照这个执行计划和存储引擎进行交互。这包括多个子阶段:解析SQL、预处理、优化SQL执行计划。这个过程中出现任何错误都可能终止查询。

语法解析器和预处理

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

预处理器则根据一些MySQL规则进一步检查树是否合法。例如检查数据表和列是否存在、名字和别名是否有歧义等待。

下一步预处理器会验证权限。这通常很快,除非服务器上有非常多的权限配置。

查询优化器

在语法树被验证合法以后,将由优化器将其转化成执行计划。一条查询可以有很多种执行方式,最后都返回相同的结果。优化器的作用就是找到这其中最好的执行计划。

MySQL使用基于成本的优化器,它将尝试预测一个查询使用某种执行计划时的成本,并选择其中成本最小的一个。当然优化器并不能完全保证所选择的执行计划是最优的,有很多种原因会导致MySQL优化器选择错误的执行计划:

  • 统计信息不准确。MySQL依赖存储引擎提供的统计信息来评估成本,但有的存储引擎提供的信息是不准确的,例如InnoBD因为其MVCC的架构,并不能维护一个数据表的精确统计信息。
  • 执行计划的成本估算不等同于实际执行的成本。例如MySQL层面并不知道哪些页面在内存中,哪些在磁盘上,所以执行的I/O时间也无从得知。所以我们看到的执行成本来选择执行计划并不是完美的模型。
  • MySQL从不考虑其他并发执行的查询。
  • MySQL也并不完全是基于成本的优化,优势也会基于一些固定的规则。例如,如果存在MATCH()子句,则在全文索引的时候就使用全文索引。即使有时候使用别的索引和WHERE条件可以远比这种方式要快。
  • MySQL不会考虑不受其控制的操作的成本。例如执行存储过程或用户自定义函数的成本。
  • 优化器有时候无法去估算所有可能的执行计划,所以可能错过实际上最优的执行计划。

MySQL的查询优化器使用了很多优化策略生成一个最优的执行计划。优化策略可以简单的分为两种:静态优化和动态优化。

  • 静态优化可以直接对解析树进行分析,并完成优化。例如优化器可以通过一些简单的代数变换将WHERE条件转换成另一种等价形式。静态优化不依赖于特别的数值,如WHERE条件中带入的一些常数等。静态优化在第一次完成之后一直有效,即使使用了不同的参数重复查询也不会发生变化,可以认为这是一种“编译时优化”。
  • 动态优化和查询的上下文有关,也可能和很多其他因素有关。例如WHERE条件的中的取值、索引中条目对应的数据行数等。这需要在每次查询的时候都重新评估,可以认为这是“运行时优化”。

MySQL对查询的静态优化只需要做一次,但对查询的动态优化规则在每次运行时都需要重新评估,有时甚至在查询的执行过程中也会重新优化。

下面是一些MySQL能够处理的优化类型:

  • 重新定义关联表的顺序:**数据表的关联并不总是按照在查询中指定的顺序进行。**决定关联的顺序是优化器很重要的一部分功能。
  • 将外连接转化成内连接:并不是所有的OUTER JOIN语句都必须以外连接的方式执行。例如WHERE条件、库表结构都可能会让外连接等价于一个内连接,MySQL可以自动识别这一点并重写查询,让其调整关联顺序。
  • 使用等价变换规则:**MySQL可以使用一些等价变换来简化并规范表达式。**它可以合并和减少一些比较,还可以移除一些恒成立和一些恒不成立的判断。
  • 优化COUNT(*)、MIN()和MAX():索引和列是否可为空通常可以帮助MySQL优化这类表达式。例如,要找到某一列的最小值,只需要查询对应的B-Tree索引最左端的记录,MySQL可以直接获取索引的第一行记录。如果MySQL使用了这种类型的优化,那么在EXPLAIN中就可以看到“Select tables optimized away”,从字面意思可以看出来,他表示优化器已经从执行计划中移除了该表,并以一个常数代替。
  • 预估并转化为常数表达式:**当MySQL检测到一个表达式可以转化为常数的时候,就会一直把该表达式作为常数进行优化处理。**
  • 索引覆盖扫描:当索引中的列包含所有查询中需要使用的列的时候,MySQL就可以使用索引返回需要的数据,而无需查询对应的数据行。
  • 子查询优化:MySQL在某些情况下可以将子查询转换一种效率更高的形式,从而减少多个查询多次对数据进行访问。
  • 提前终止查询:**在发现已经满足查询需求的时候,MySQL总是能够立刻终止查询。**例如当使用了LIMIT的时候、发现了一个不成立的条件等。
  • 等值传播:如果两个列的值通过等式关联,那么MySQL能够把其中一个列的WHERE条件传递到另一个列上。
  • 列表IN()的比较:在很多数据库系统中,IN()完全等同于多个OR条件的子句,因为这二者完全等价。但是在MySQL中这点是不成立的,MySQL将IN()列表中的数据先进行排序,然后通过二分查找的方式来确定列表中的值是否满足条件,这是一个O(log n)复杂度的操作,等价的变换成OR查询的复杂度为O(n),对IN()列表中有大量取值的时候,MySQL的处理将会更快。

数据和索引的统计信息

MySQL服务层没有任何统计信息,所以MySQL查询优化器在生成查询的计划时,需要向存储引擎获取相应的统计信息。存储引擎则提供给优化器对应的统计信息,包括:每个表或者索引有多少个页面、每个表的每个索引的基数是多少、数据行和索引长度、索引的分布信息等。优化器根据这些信息来选择一个最优的执行计划。

MySQL如何执行关联查询

MySQL认为任何一个查询都是一次“关联”,并不仅仅是一个查询需要到两个表匹配才叫关联,所以在MySQL中,每一次查询,每一个片段都可能是关联。

举一个UNION查询的例子,对于UNION查询,MySQL先将一系列的单个查询结果放在一个临时表中,然后再重新读出临时表数据来完成UNION查询,所以读取结果临时也是一次关联。

当前MySQL关联执行的策略很简单:MySQL对任何关联都执行嵌套循环关联操作,即MySQL先在一个表中循环取出单条数据,然后再嵌套循环到下一个表中寻找匹配的行,依次下去,直到找到所有表中匹配的行为止。然后根据各个表匹配的行,返回查询中需要的各个列。MySQL会先尝试在最后一个关联表中找到所有匹配的行,如果最后一个关联表无法找到更多的行以后,MySQL返回到上一层关联表,看是否能找到更多的匹配记录,依次类推迭代执行。

从本质上来说,MySQL对所有的类型的查询都以同样的方式运行。例如,MySQL在FROM子句中遇到子查询时,先执行子查询并将其结果放在一个临时表中,然后将这个临时表当作一个普通表对待(正如其名“派生表”)。MySQL在执行UNION查询时也是用类似的临时表。在遇到右外连接的时候,MySQL将其改写成等价的左外连接。但是全外连接无法通过嵌套循环和回溯的方式完成,这大概也是MySQL并不支持全外连接的原因。

执行计划

和很多其他关系数据库不同,MySQL并不会生成查询字节码来执行查询,MySQL生成查询一棵指令树,然后通过存储引擎执行完成这棵指令树并返回结果。MySQL总是从一个表开始一直嵌套循环、回溯完成所有表关联。所以MySQL的执行计划总是如下图所示,是一棵左侧深度优先的树:

关联查询优化器

MySQL优化器最重要的一部分就是关联查询优化,他决定了多个表关联时的顺序。通常多表关联的时候,可以有多种不同的关联顺序来获得相同的执行顺序,关联查询优化器则通过评估不同顺序时的成本来选择一个代价最小的关联顺序。

关联优化器会尝试再所有的关联顺序中选择一个成本最小的来生成执行计划,尽量保证每次关联查询后,能排除掉更多的无用数据。如果可能,优化器会遍历每一个表然后逐个做嵌套循环计算每一棵可能的执行计划树的成本,最后返回一个最优的执行计划。

不过糟糕的是,如果有超过n个表的关联,那么需要检查n的阶乘种关联计划。我们称之为所有可能的执行计划的“搜索空间”,搜索空间的增长速度非常快,例如若10个表的关联,那么共有3628800种不同的关联顺序。

排序优化

无论如何排序都是一个成本很高的操作,所以从性能考虑,应尽可能避免排序或者尽可能对大量数据进行排序。

当不能使用索引生成排序结果的时候,MySQL需要自己进行排序,如果数据量小则在内存中进行,如果数据量大则需要使用磁盘,不过MySQL将这个过程统一称为文件排序(filesort)。

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

MySQL有两种排序算法:

  • 两次传输排序(旧版本使用):**读取行指针和需要排序的字段,对其进行排序,然后再根据排序结果读取所需要的数据行。** 这需要进行两次数据传输,即需要从数据表中读取两次数据,第二次读取数据的时候,因为是读取排序列进行排序后的所有记录,这会产生大量的随机I/O,所以两次数据传输的成本非常高。
  • 单次传输排序(新版本使用):**先读取所需要的所有列,然后再根据给定列进行排序,最后直接返回排序结果。因为不需要从数据表中读取两次数据,对于I/O密集型的应用,这样做的效率高了很多。但是缺点是如果需要返回的列非常多、非常大,会额外占用大量的空间,**而这些列对排序操作本身来说是没有任何作用的。

MySQL在进行文件排序的时候需要使用的临时存储空间可能会比想象的要大很多,原因在于MySQL在排序时,对每一个排序记录都会分配一个足够长的定长空间来存放。这个定长空间必须足够长以容纳其中最长的字符串,如果使用UTF-8字符集,那么MySQL将会为每个字符预留三个字节。因此排序消耗的临时空间可能会比磁盘上的原表要大很多倍。

在关联查询的时候如果需要排序,MySQL会分两种情况来处理这样的文件排序。如ORDER BY子句中所有的列都来自关联的第一个表,那么MySQL在关联处理第一个表的时候就进行文件排序,这种情况MySQL在EXPLAIN结果中的Extra字段会有“Using filesort”。除此之外所有情况,MySQL都会先将关联的结果存放到一个临时表中,然后在所有的关联都结束后,再进行文件排序,这种情况MySQL在EXPLAIN结果中的Extra字段会有“Using temporary;Using filesort”。如果查询中有LIMIT的话,LIMIT也会在排序之后应用。

MySQL5.6后,当只需要返回部分排序结果的时候,例如使用了LIMIT子句,MySQL不再对所有的结果进行排序,而是根据实际情况,选择抛弃不满足条件的结果,然后再进行排序。

查询执行引擎

在解析和优化阶段,MySQL将生成查询对应的执行计划,MySQL的查询执行引擎则根据这个执行计划来完成整个查询。这里执行计划是一个数据结构,而不是和很多其他关系型数据库那样会生成对应的字节码。

MySQL只是简单的根据执行计划给出的指令逐行执行,在根据执行计划逐步执行的过程中,有大量的操作需要通过调用存储引擎实现的接口来完成。

返回结果给客户端

查询执行的最后一个阶段是将结果返回给客户端。即使查询不需要返回结果集给客户端,MySQL仍然会返回这个查询的一些信息,如该查询影响到的行数。

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

MySQL将结果集返回给客户端是一个增量、逐步返回的过程,一旦服务器处理完最后一个关联表,开始生成第一条结果时,MySQL就可以开始向客户端逐步返回结果集了。

这样处理有两个好处:服务器无须存储太多的结果,也就不会因为要返回太多的结果而消耗太多内存。另外这样的处理也让MySQL客户端第一时间获得返回的结果。