前言
简单描述一下多表怎么连接的。
正文
首先,我们得抛开我们一些自以为是的想法。
我想过这个问题,就是为什么我们背乘法口诀的时候,我们总是背:
22 = 4, 99=81 这样背下去,似乎这是口诀。然而这是缓存,不是计算,既然不是计算那么就不是逻辑学。
我们理所当然的想9*9=81,而抛弃了运算过程。
这样短期是有好处的,那就是我们马上知道99=81,这似乎是好处,那么坏处是啥,那就是我们抛弃了本质问题,为什么99的计算过程。
而计算机就是描述计算过程的机器。
比方说,我们在多表连接的时候就会想,怎么怎么去实现它。
比如select * from t1,t2 where t1.id= t2.t1Id and t1.a > 50 and t2.b > 20
我们会去想如何能够让计算机快速得出我们的答案呢。
我们肯定会想,同时进行是不是很好呢?比如说把t1.a >50 的找出来,同时按照t2.b >20 的找出来。
然后进行在根据条件t1.id= t2.t1Id 进行条件拼接。
是的,是的,我们这个时候又会想如果一个一个区遍历t1的结果,然后每一个去找到t2的结果似乎很慢,如何能减少遍历次数呢?
是的,是的,没错,我们脑海中第一印象就是想着如何如何去优化。而且是否我们最开始的优化想法,是否是优化的呢?
比如说把t1.a >50 的找出来,同时按照t2.b >20 的找出来,这个是否同时做就是优化呢?
先来看一下最最最简单的实现,一切都是串行, 简单的逻辑去思考问题。
第一步呢,我们首先查出t1.a>50 条的出来,比如说有两条,比如a=51和a=52,id 是10和15
那么可以根据这两条,去b中查询。
b 中怎么查询呢? b的查询方式是t1.a = t2.b查询。
比如第一条,id 是10,那么就查询t2中t1Id为10的,查出来后再根据t2.b >20进行过滤。
第二条同理。
也就是说在两表连接查询中,驱动表只需要访问一次,被驱动表可能被访问多次。
这似乎是可以实现我们的东西,对于人来说似乎是复杂的,但是对于机器而言会不厌其烦。
这里有个问题,那就是我们以t1为驱动表,那么为啥是t1呢? 有没有可能是t2呢?
嗯,似乎是可以的。假如t2.b >20只查出一条的话,那么是不是性能更高呢。
似乎t1还是t2做驱动表似乎是可以根据一些参数做一些优化的。
但是是不是全部的连接,都可以根据参数去做优化的呢?
其实只有内连接系统可以自我去优化。
除了内连接还有外连接呢。
那么什么是外连接呢? 请百度。
外连接如果是左连接,那么就是左边的作为驱动表,如果是右连接就右边的作为驱动表。
如果你了解什么是外连接的,那么这样实现是没有问题的。
现在想想该怎么优化呢?
第一阶段我们查询t1,似乎我们没有啥好优化的,那么能不能从第二阶段下文章呢?
第二阶段我们可以分成两条语句。
select * from t2 where t2.t1Id = 20 and t2.b> 20select * from t2 where t2.t1Id = 30 and t2.b> 20
对于t2的查询,那么我们似乎可以创建索引。这也是为啥我们外键会创建索引。
假如我们t1Id创建索引的话,那么是不是就很快了。
然要并非一定要如此,t2.b 创建也是可以的,只是这会条件就是t2.t1Id = 20了。
然后我们知道t2.t1Id = 20 如果是唯一索引或者说是主键索引,那么就是查询方式是const,但是如果作为被驱动表,那么名字就叫做eq_ref了。
另外,有时候连接查询的查询列表和过滤条件中可能只涉及被驱动表的部分列,而这些列都是某个索引的一部
分,这种情况下即使不能使用 eq_ref 、 ref 、 ref_or_null 或者 range 这些访问方法执行对被驱动表的查询的
话,也可以使用索引扫描,也就是 index 的访问方法来查询被驱动表。所以我们建议在真实工作中最好不要使
用 * 作为查询列表,最好把真实用到的列作为查询列表。
那么还有没有其他可以优化的地方呢?
这里我们知道,我们查询出来一条就要和驱动表匹配一次,这似乎有点点不太如意。
这有索引的情况下,其实还好,但是如果没有索引呢?那么我们是不是得去全表扫描。
如果一条驱动表的记录扫描一次,那么如果很多很多呢?那么不是得扫描很多很多次。
这似乎很糟糕。
那么如何才能不那么糟糕呢? 那就是批量取呗。
当被驱动表中的数据非常多时,每次访问被驱动表,被驱动表的记录会被加载到内存中,在内存中的每一条记录
只会和驱动表结果集的一条记录做匹配,之后就会被从内存中清除掉。然后再从驱动表结果集中拿出另一条记
录,再一次把被驱动表的记录加载到内存中一遍,周而复始,驱动表结果集中有多少条记录,就得把被驱动表从
磁盘上加载到内存中多少次。所以我们可不可以在把被驱动表的记录加载到内存的时候,一次性和多条驱动表中
的记录做匹配,这样就可以大大减少重复从磁盘上加载被驱动表的代价了。所以设计 MySQL 的大叔提出了一个
join buffer 的概念, join buffer 就是执行连接查询前申请的一块固定大小的内存,先把若干条驱动表结果集
中的记录装在这个 join buffer 中,然后开始扫描被驱动表,每一条被驱动表的记录一次性和 join buffer 中的
多条驱动表记录做匹配,因为匹配的过程都是在内存中完成的,所以这样可以显著减少被驱动表的 I/O 代价。使
用 join buffer 的过程如下图所示:
最好的情况是 join buffer 足够大,能容纳驱动表结果集中的所有记录,这样只需要访问一次被驱动表就可以完
成连接操作了。设计 MySQL 的大叔把这种加入了 join buffer 的嵌套循环连接算法称之为 基于块的嵌套连接
(Block Nested-Loop Join)算法。
这个 join buffer 的大小是可以通过启动参数或者系统变量 join_buffer_size 进行配置,默认大小为 262144字
节 (也就是 256KB ),最小可以设置为 128字节 。当然,对于优化被驱动表的查询来说,最好是为被驱动表加
上效率高的索引,如果实在不能使用索引,并且自己的机器的内存也比较大可以尝试调大 join_buffer_size 的
值来对连接查询进行优化。
另外需要注意的是,驱动表的记录并不是所有列都会被放到 join buffer 中,只有查询列表中的列和过滤条件中
的列才会被放到 join buffer 中,所以再次提醒我们,最好不要把 * 作为查询列表,只需要把我们关心的列放到
查询列表就好了,这样还可以在 join buffer 中放置更多的记录呢哈。
其实最好还是避免全表扫描,建立对应的索引更好,尽量不要用到block nested-loop join.
结
简单介绍一下连接的原理。