在开始阅读源码以前, 咱们来看看什么是 Sort Merge Join (SMJ),定义能够看 wikipedia。简单说来就是将 Join 的两个表,首先根据链接属性进行排序,而后进行一次扫描归并, 进而就能够得出最后的结果。这个算法最大的消耗在于对内外表数据进行排序,而当链接列为索引列时,咱们能够利用索引的有序性避免排序带来的消耗, 因此一般在查询优化器中,链接列为索引列的状况下能够考虑选择使用 SMJ。git
TiDB 的实现代码在 tidb/executor/merge_join.go 中 MergeJoinExec.NextChunk
是这个算子的入口。下面以 SELECT * FROM A JOIN B ON A.a = B.a
为例,对 SMJ 执行过程进行简述,假设此时外表为 A,内表为 B,join-keys 为 a,A,B 表的 a 列上都有索引:github
顺序读取外表 A 直到 join-keys 中出现另外的值,把相同 keys 的行放入数组 a1,一样的规则读取内表 B,把相同 keys 的行放入数组 a2。若是外表数据或者内表数据读取结束,退出。算法
从 a1 中读取当前第一行数据,设为 v1。从 a2 中读取当前第一行数据,设为 v2。express
根据 join-keys 比较 v1,v2,结果分为几种状况:数组
回到步骤 1。框架
下面的图展现了 SMJ 的过程:函数
咱们分别经过 fetchNextInnerRows
或者 fetchNextOuterRows
读取内表和外表的数据。这两个函数实现的功能相似,这里只详述函数 fetchNextInnerRows
的实现。源码分析
MergeSortExec
算子读取数据,是经过迭代器 readerIterator
完成,readerIterator
能够顺序读取数据。MergeSortExec
算子维护两个 readerIterator:outerIter
和 innerIter
,它们在 buildMergeJoin
函数中被构造。fetch
真正读取数据的操做是在 readerIterator.nextSelectedRow
中完成, 这里会经过 ri.reader.NextChunk
每次读取一个 Chunk 的数据,关于 Chunk 的相关内容,能够查看咱们以前的文章 TiDB 源码阅读系列文章(十)Chunk 和执行框架简介 。优化
这里值得注意的是,咱们经过 expression.VectorizedFilter
对外表数据进行过滤,返回一个 curSelected 布尔数组,用于外表的每一行数据是不是知足 filter 过滤条件。以 select * from t1 left outer join t2 on t1.a=100;
为例, 这里的 filter 是 t1.a=100
, 对于没有经过这个过滤条件的行,咱们经过 ri.joinResultGenerator.emitToChunk
函数发送给 resultGenerator, 这个 resultGenerator 是一个 interface,具体是否输出这行数据,会由 join 的类型决定,好比外链接则会输出,内链接则会忽略。具体关于 resultGenerator, 能够参考以前的文章:TiDB 源码阅读系列文章(九)Hash Join
rowsWithSameKey
经过 nextSelectedRow
不断读取下一行数据,并经过对每行数据的 join-keys 进行判断是否是属于同一个 join-keys,若是是,会把相同 join-keys 的行分别放入到 innerChunkRows
和 outerIter4Row
数组中。而后对其分别创建迭代器 innerIter4Row 和 outerIter4Row。在 SMJ 中的执行过程当中,会利用这两个迭代器来获取数据进行真正的比较得出 join result。
实现 Merge-Join 逻辑的代码在函数 MergeJoinExec.joinToChunk
, 对内外表迭代器的当前数据根据各自的 join-keys 做对比,有以下几个结果:
cmpResult > 0,表明外表当前数据大于内表数据,那么经过 fetchNextInnerRows
直接读取下一个内表数据,而后从新比较便可。
cmpResult < 0,表明外表当前数据小于内表数据,这个时候就分几种状况了,若是是外链接,那么须要输出外表数据 + NULL,若是是内链接,那么这个外表数据就被忽略,对于这个不一样逻辑的处理,统一由 e.resultGenerator
来控制,咱们只须要把外表数据经过 e.resultGenerator.emitToChunk
调用它便可。而后经过 fetchNextOuterRows
读取下一个外表数据,从新比较。
cmpResult == 0,表明外表当前数据等于内表当前数据,这个时候就把外表数据跟内表当前数据作一次链接,经过 e.resultGenerator.emitToChunk
生成结果。以后外表跟内表分别获取下一个数据,从新开始比较。
重复上面的过程,直到外表或者内表数据被遍历完,退出 Merge-Join 的过程。
咱们上面的分析代码基于 Source-code 分支,可能你们已经发现了一些问题,好比咱们会一次性读取内外表的 Join group(相同的 key)。这里若是相同的 key 比较多,是有内存 OOM 的风险的。针对这个问题,咱们在最新的 master 分支作了几个事情来优化:
外表其实不须要把相同的 keys 一次性都读取上来, 它只须要按次迭代外表数据,再跟内表逐一对比做链接便可。这里至少能够减小外表发生 OOM 的问题,能够大大减小 OOM 的几率。
对于内表,咱们对 OOM 也不是没有办法,咱们用 memory.Tracker
这个内存追踪器来记录当前内表已经使用的中间结果的内存大小,若是它超过咱们设置的阈值,咱们会采起输出日志或者终止 SQL 继续运行的方法来规避 OOM 的发生。关于 memory.Tracker
咱们不在此展开,能够留意咱们后续的源码分析文章。
后续咱们还会在 Merge-Join 方面作一些优化, 好比咱们能够作多路归并,中间结果存外存等等,敬请期待。
做者:姚维