TiDB 源码阅读系列文章(十五)Sort Merge Join

2024-04-08 03:18

本文主要是介绍TiDB 源码阅读系列文章(十五)Sort Merge Join,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

什么是 Sort Merge Join

在开始阅读源码之前, 我们来看看什么是 Sort Merge Join (SMJ),定义可以看 wikipedia。简单说来就是将 Join 的两个表,首先根据连接属性进行排序,然后进行一次扫描归并, 进而就可以得出最后的结果。这个算法最大的消耗在于对内外表数据进行排序,而当连接列为索引列时,我们可以利用索引的有序性避免排序带来的消耗, 所以通常在查询优化器中,连接列为索引列的情况下可以考虑选择使用 SMJ。

TiDB Sort Merge Join 实现

执行过程

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 列上都有索引:

  1. 顺序读取外表 A 直到 join-keys 中出现另外的值,把相同 keys 的行放入数组 a1,同样的规则读取内表 B,把相同 keys 的行放入数组 a2。如果外表数据或者内表数据读取结束,退出。

  2. 从 a1 中读取当前第一行数据,设为 v1。从 a2 中读取当前第一行数据,设为 v2。

  3. 根据 join-keys 比较 v1,v2,结果分为几种情况:

    • cmpResult > 0, 表示 v1 大于 v2,把当前 a2 的数据丢弃,从内表读取下一批数据,读取方法同 1。重复 2。
    • cmpResult < 0, 表示 v1 小于 v2,说明外表的 v1 没有内表的值与之相同,把外表数据输出给 resultGenerator(不同的连接类型会有不同的结果输出,例如外连接会把不匹配的外表数据输出)。
    • cmpResult == 0, 表示 v1 等于 v2。那么遍历 a1 里面的数据,跟 a2 的数据,输出给 resultGenerator 作一次连接。
  4. 回到步骤 1。

下面的图展示了 SMJ 的过程:

图 1 SMJ 过程.png

读取内表 / 外表数据

我们分别通过 fetchNextInnerRows 或者 fetchNextOuterRows 读取内表和外表的数据。这两个函数实现的功能类似,这里只详述函数 fetchNextInnerRows 的实现。

MergeSortExec 算子读取数据,是通过迭代器 readerIterator 完成,readerIterator 可以顺序读取数据。MergeSortExec 算子维护两个 readerIterator:outerIterinnerIter,它们在 buildMergeJoin 函数中被构造。

真正读取数据的操作是在 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 的行分别放入到 innerChunkRowsouterIter4Row 数组中。然后对其分别建立迭代器 innerIter4Row 和 outerIter4Row。在 SMJ 中的执行过程中,会利用这两个迭代器来获取数据进行真正的比较得出 join result。

Merge-Join

实现 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 分支做了几个事情来优化:

  1. 外表其实不需要把相同的 keys 一次性都读取上来, 它只需要按次迭代外表数据,再跟内表逐一对比作连接即可。这里至少可以减少外表发生 OOM 的问题,可以大大减少 OOM 的概率。

  2. 对于内表,我们对 OOM 也不是没有办法,我们用 memory.Tracker 这个内存追踪器来记录当前内表已经使用的中间结果的内存大小,如果它超过我们设置的阈值,我们会采取输出日志或者终止 SQL 继续运行的方法来规避 OOM 的发生。关于 memory.Tracker 我们不在此展开,可以留意我们后续的源码分析文章。

后续我们还会在 Merge-Join 方面做一些优化, 比如我们可以做多路归并,中间结果存外存等等,敬请期待。

作者:姚维

这篇关于TiDB 源码阅读系列文章(十五)Sort Merge Join的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/884428

相关文章

SQL中JOIN操作的条件使用总结与实践

《SQL中JOIN操作的条件使用总结与实践》在SQL查询中,JOIN操作是多表关联的核心工具,本文将从原理,场景和最佳实践三个方面总结JOIN条件的使用规则,希望可以帮助开发者精准控制查询逻辑... 目录一、ON与WHERE的本质区别二、场景化条件使用规则三、最佳实践建议1.优先使用ON条件2.WHERE用

Mybatis Plus Join使用方法示例详解

《MybatisPlusJoin使用方法示例详解》:本文主要介绍MybatisPlusJoin使用方法示例详解,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,... 目录1、pom文件2、yaml配置文件3、分页插件4、示例代码:5、测试代码6、和PageHelper结合6

java String.join()方法实例详解

《javaString.join()方法实例详解》String.join()是Java提供的一个实用方法,用于将多个字符串按照指定的分隔符连接成一个字符串,这一方法是Java8中引入的,极大地简化了... 目录bVARxMJava String.join() 方法详解1. 方法定义2. 基本用法2.1 拼接

8种快速易用的Python Matplotlib数据可视化方法汇总(附源码)

《8种快速易用的PythonMatplotlib数据可视化方法汇总(附源码)》你是否曾经面对一堆复杂的数据,却不知道如何让它们变得直观易懂?别慌,Python的Matplotlib库是你数据可视化的... 目录引言1. 折线图(Line Plot)——趋势分析2. 柱状图(Bar Chart)——对比分析3

Android实现一键录屏功能(附源码)

《Android实现一键录屏功能(附源码)》在Android5.0及以上版本,系统提供了MediaProjectionAPI,允许应用在用户授权下录制屏幕内容并输出到视频文件,所以本文将基于此实现一个... 目录一、项目介绍二、相关技术与原理三、系统权限与用户授权四、项目架构与流程五、环境配置与依赖六、完整

Android实现定时任务的几种方式汇总(附源码)

《Android实现定时任务的几种方式汇总(附源码)》在Android应用中,定时任务(ScheduledTask)的需求几乎无处不在:从定时刷新数据、定时备份、定时推送通知,到夜间静默下载、循环执行... 目录一、项目介绍1. 背景与意义二、相关基础知识与系统约束三、方案一:Handler.postDel

MySQL 多表连接操作方法(INNER JOIN、LEFT JOIN、RIGHT JOIN、FULL OUTER JOIN)

《MySQL多表连接操作方法(INNERJOIN、LEFTJOIN、RIGHTJOIN、FULLOUTERJOIN)》多表连接是一种将两个或多个表中的数据组合在一起的SQL操作,通过连接,... 目录一、 什么是多表连接?二、 mysql 支持的连接类型三、 多表连接的语法四、实战示例 数据准备五、连接的性

Java 正则表达式URL 匹配与源码全解析

《Java正则表达式URL匹配与源码全解析》在Web应用开发中,我们经常需要对URL进行格式验证,今天我们结合Java的Pattern和Matcher类,深入理解正则表达式在实际应用中... 目录1.正则表达式分解:2. 添加域名匹配 (2)3. 添加路径和查询参数匹配 (3) 4. 最终优化版本5.设计思

MySQL高级查询之JOIN、子查询、窗口函数实际案例

《MySQL高级查询之JOIN、子查询、窗口函数实际案例》:本文主要介绍MySQL高级查询之JOIN、子查询、窗口函数实际案例的相关资料,JOIN用于多表关联查询,子查询用于数据筛选和过滤,窗口函... 目录前言1. JOIN(连接查询)1.1 内连接(INNER JOIN)1.2 左连接(LEFT JOI

C++ Sort函数使用场景分析

《C++Sort函数使用场景分析》sort函数是algorithm库下的一个函数,sort函数是不稳定的,即大小相同的元素在排序后相对顺序可能发生改变,如果某些场景需要保持相同元素间的相对顺序,可使... 目录C++ Sort函数详解一、sort函数调用的两种方式二、sort函数使用场景三、sort函数排序