Join节点

JOIN节点有以下三种:

	T_NestLoopState,
T_MergeJoinState,
T_HashJoinState,

连接类型节点对应于关系代数中的连接操作,PostgreSQL中定义了如下几种连接类型(以T1 JOIN T2 为例):

  • 1)Inner Join:内连接,将T1的所有元组与T2中所有满足连接条件的元组进行连接操作。

  • 2)Left Outer Join:左连接,在内连接的基础上,对于那些找不到可连接T2元组的T1元组,用一个空值元组与之连接。

  • 3)Right Outer Join:右连接,在内连接的基础上,对于那些找不到可连接T1元组的T2元组,用一个空值元组与之连接。

  • 4)Full Outer Join:全外连接,在内连接的基础上,对于那些找不到可连接T2元组的T1元组,以及那些找不到可连接T1元组的T2元组,都要用一个空值元组与之连接。

  • 5)Semi Join:类似IN操作,当T1的一个元组在T2中能够找到一个满足连接条件的元组时,返回该T1元组,但并不与匹配的T2元组连接。

  • 6)Anti Join:类型NOT IN操作,当T1的一个元组在T2中未找到满足连接条件的元组时,返回该T1元组与空元组的连接。

我们再看看postgres用户手册里是怎么说的:

条件连接:

T1 { [INNER] | { LEFT | RIGHT | FULL } [OUTER] } JOIN T2 ON boolean_expression
T1 { [INNER] | { LEFT | RIGHT | FULL } [OUTER] } JOIN T2 USING ( join column list )
T1 NATURAL { [INNER] | { LEFT | RIGHT | FULL } [OUTER] } JOIN T2

这样看起来,只有头四种连接方式(INNER, LEFT JOIN, RIGHT JOIN, FULL JOIN)在SQL语句中显示使用了,后两种其实是作为postgres内部使用的,例如Semi Join,我之前说过对于SubqueryScan节点,有可能把ANY和EXIST子句转换为半连接。半连接就是Semi Join。

而对于你所指定的连接方式,PostgreSQL内部会见机行事,使用不同的连接操作

这里,postgres实现了三种连接操作,分别是:嵌套循环连接(Nest Loop)、归并连接(Merge Join)和Hash连接(Hash Join)。

如下所示,连接节点有公共父类Join, Join继承了 Plan的所有属性,并扩展定义了 jointype用以存储连接的类型,joinqual用于存储连接的条件。

typedef struct Join
{
Plan plan;
JoinType jointype;
List *joinqual; /* JOIN quals (in addition to plan.qual) */
} Join;

对应的执行状态节点JoinState中定义了jointype存储连接类型,joinqual存储连接条件初始化后的状态链表。

typedef struct JoinState
{
PlanState ps;
JoinType jointype;
List *joinqual; /* JOIN quals (in addition to ps.qual) */
} JoinState;

1.NestLoop节点

NestLoop节点实现了嵌套循环连接方法,能够进行Inner Join、Left Outer Join、Semi Join和Anti Join四种连接方式。

举例如下:

postgres=# explain select a.*,b.* from test_dm a join test_dm2 b on a.id > b.id;
QUERY PLAN
--------------------------------------------------------------------------------
Nested Loop (cost=0.00..150000503303.17 rows=3333339000000 width=137)
Join Filter: (a.id > b.id)
-> Seq Scan on test_dm2 b (cost=0.00..223457.17 rows=10000017 width=69)
-> Materialize (cost=0.00..27346.00 rows=1000000 width=68)
-> Seq Scan on test_dm a (cost=0.00..22346.00 rows=1000000 width=68)
(5 行)
typedef struct NestLoop
{
Join join;
List *nestParams; /* list of NestLoopParam nodes */
} NestLoop;

NestLoop节点在Join节点的基础上扩展了nestParams字段,这个字段nestParams是一些执行器参数的列表,这些参数的用处是将外部子计划的当前行执行值传递到内部子计划中。目前主要的传递形式是Var型,这个数据结构的定义在:

src/include/nodes/primnodes.h
Var - expression node representing a variable (ie, a table column)

下面是状态节点NestLoopState的定义。

typedef struct NestLoopState
{
JoinState js; /* its first field is NodeTag */
bool nl_NeedNewOuter; //true if need new outer tuple on next call
bool nl_MatchedOuter; //true if found a join match for current outer tuple
TupleTableSlot *nl_NullInnerTupleSlot; //prepared null tuple for left outer joins
} NestLoopState;

NestLoop节点的初始化过程(ExecEndNestLoop函数)中初始化NestLoopState节点,构造表达式上下文这些自不必说,还会对节点中连接条件(joinqual字段)进行处理,转化为对应的状态节点JoinState中的joinqual链表。并且对于LEFT JOIN和ANTI JOIN会初始化一个nl_NullInnerTupleSlot。why?

因为对于T1 JOIN T2,当T1的一个元组在T2中未找到满足连接条件的元组时,这两种连接方式会返回该T1元组与空元组的连接,这个空元组就是由nl_NullInnerTupleSlot实现。

最后还将进行如下两个操作:

  • 1)将nl_NeedNewOuter标记为true,表示需要获取左子节点元组。

  • 2)将nl_MatchedOuter标记为false,表示没有找到与当前左子节点元组匹配的右子节点元组。

初始化就是这些。

接下来是NESTLOOP的执行过程(ExecNestLoop函数)。

循环嵌套连接的基本思想如下(以表R(左关系)与表S(右关系)连接为例):

	FOR each tuple s in S DO
FOR each tuple r in R DO IF r and s join to make a tuple t THEN
output t;

为了迭代实现此方法,NestLoopState中定义了字段nl_NeedNewOuter和nl_MatchedOuter。当元组处于内层循环时,nl_NeedNewOuter为false,内层循环结束时nl_NeedNewOuter设置为true。为了能够处理Left Outer Join和Anti Join,需要知道内层循环是否找到了满足连接条件的内层元组,此信息由nl_MatchedOuter记录,当内层循环找到符合条件的元组时将其标记为true。

NestLoop执行过程主要是由ExecNestLoop函数来做。该函数主要是一个如上面提到的一个大循环。

该循环执行如下操作:

  • <1>如果nl_NeedNewOuter为true,则从左子节点获取元组,若获取的元组为NULL则返回空元组并结束执行过程。如果nLNeedNewOuter为false,则继续进行步骤2。

  • <2>从右子节点获取元组,若为NULL表明内层扫描完成,设置nl_NeedNewOuter为true,跳过步骤3继续循环。

  • <3>判断右子节点元组是否与当前左子节点元组符合连接条件,若符合则返回连接结果。

以上过程能够完成Inner Join的递归执行过程。但是为了支持其他几种连接则还需要如下两个特殊的处理:

  • 1)当找到符合连接条件的元组后将nl_MatchedOuter标记为true。内层扫描完毕时,通过判断nl_MatchedOuter即可知道是否已经找到满足连接条件的元组,在处理Left Outer Join和Anti Join时需要进行与空元组(nl_NullInnerTupleSlot)的连接,然后将nLMatchedOuter设置为false。

  • 2)当找到满足匹配条件的元组后,对于Semi JOIN和Anti JOIN方法需要设置nl_NeedNewOuter为true。区别在于Anti Join需要不满足连接条件才能返回,所以要跳过返回连接结果继续执行循环。

NestLoop节点的清理过程(ExecEndNestLoop函数)没有特殊处理,只需递归调用左右子节点的清理过程。


2.MergeJoin 节点

Merge Join先要对各表各自排序,然后从各自的排序表中抽取数据,到另一个排序表中做匹配。通常来讲,能够使用merge join的地方,hash join 更快。

所以,为了比较容易地再现Merge Join:我们先禁止Hash Join,在执行SQL:

postgres=# set session enable_hashjoin=false;
SET postgres=# explain select a.*,b.* from test_dm a join test_dm2 b on a.xxx = b.xxx;
QUERY PLAN
-----------------------------------------------------------------------------------
Merge Join (cost=1508137.95..1573138.03 rows=1000000 width=137)
Merge Cond: ((a.xxx)::text = (b.xxx)::text)
-> Sort (cost=122003.84..124503.84 rows=1000000 width=68)
Sort Key: a.xxx
-> Seq Scan on test_dm a (cost=0.00..22346.00 rows=1000000 width=68)
-> Sort (cost=1386134.10..1411134.14 rows=10000017 width=69)
Sort Key: b.xxx
-> Seq Scan on test_dm2 b (cost=0.00..223457.17 rows=10000017 width=69)
(8 行)

Mergejoin实现了对排序关系的归并连接算法,归并连接的输人都是已经排好序的。PostgreSQL中Mergejoin算法实现的伪代码如下:

 	Join {
get initial outer and inner tuples INITIALIZE
do forever {
while (outer != inner) { SKIP_TEST
if (outer < inner)
advance outer SKIPOUTER_ADVANCE
else
advance inner SKIPINNER_ADVANCE
}
mark inner position SKIP_TEST
do forever {
while (outer == inner) {
join tuples JOINTUPLES
advance inner position NEXTINNER
}
advance outer position NEXTOUTER
if (outer == mark) TESTOUTER
restore inner position to mark TESTOUTER
else
break // return to top of outer loop
}
}
}

我们知道Mergejoin是一个双重循环,只不过这个循环比较复杂,注意上面伪代码右边的大写英文单词,他其实就是对应了Mergejoin节点执行过程中的“状态”。这些状态在代码里就是下面这些宏,实际的代码里就是靠这些宏表示不同的状态,从而正对不同的状态做出相应的处理,使的代码逻辑清晰明了。

#define EXEC_MJ_INITIALIZE_OUTER		1
#define EXEC_MJ_INITIALIZE_INNER 2
#define EXEC_MJ_JOINTUPLES 3
#define EXEC_MJ_NEXTOUTER 4
#define EXEC_MJ_TESTOUTER 5
#define EXEC_MJ_NEXTINNER 6
#define EXEC_MJ_SKIP_TEST 7
#define EXEC_MJ_SKIPOUTER_ADVANCE 8
#define EXEC_MJ_SKIPINNER_ADVANCE 9
#define EXEC_MJ_ENDOUTER 10
#define EXEC_MJ_ENDINNER 11

算法首先初始化左右子节点,然后执行以下操作(其中对于大小的比较都是指对连接属性值的比较):

  • 1)扫描到第一个匹配的位置,如果左子节点(outer)较大,从右子节点(inner)中获取元组;如果右子节点较大,从左子节点中获取元组。

  • 2)标记右子节点当前的位置。

  • 3)循环执行左子节点==右子节点判断,若符合则连接元组,并获取下一条右子节点元组,否则退出循环执行步骤4。

  • 4)获取下一条左子节点元组。

  • 5)如果左子节点==标记处的右子节点(说明该条左子节点与上一条相等),需要将右子节点扫描位置回退到扫描位置,并返冋步骤3;否则跳转到步骤1。

为了说明归并排序的连接算法,我们以Inner Join(即内连接)为例给出部分执行过程,两个current分别指向输人的当前元组,mark用于标记扫描的位置。

1)首先找到左右序列第一个匹配位置,下图中current(outer)=0小于Current(inner),因此outer的current向后移动。

2)如图所示,当找到匹配项后,则进行连接,使用mark标记当前inner的扫描位置,并将inner的current向后移动。

3)接着判断current(outer) = 1小于current(inner) =2,则将outer的current向后移动,并判断outer是否与mark相同(这是为了发现outer的current与前一个相同的情况)。

4)下图显示current(outer) =2不等于mark(inner) = 1,则继续扫描过程。

5)判断两个current是否相同,发现Currem(outer)=2等于current(inner)=2,则进行连接,同样标记inner的当前位置,并将inner的cuirent向后移动,如下图所示。其中的current(inner) = 2仍满足连接条件,因此连接完成后inner的current继续向后移动。

6)如下图所示,current(outer)=2 小于current(inner)=5,则将 outer的current指针向后移动。

7)此时判断current(outer)和mark(inner)相等,则将inner的current指向mark的位置,重新获取inner的元组进行匹配,如下图所示。

8)不断重复这样的匹配模式,直到inner或outer中的一方被扫描完毕,则表示连接完成。

MergeJoin节点的定义如下:

typedef struct MergeJoin
{
Join join;
List *mergeclauses; /* mergeclauses as expression trees */
/* these are arrays, but have the same length as the mergeclauses list: */
Oid *mergeFamilies; /* per-clause OIDs of btree opfamilies */
Oid *mergeCollations; /* per-clause OIDs of collations */
int *mergeStrategies; /* per-clause ordering (ASC or DESC) */
bool *mergeNullsFirst; /* per-clause nulls ordering */
} MergeJoin;

该节点在join的基础上扩展定义了几个mergexxx字段。其中mergeclauses存储用于计算左右子节点元组是否匹配的表达式链表,mergeFamilies、mergeCollations、mergeStrategies、mergeNullsFirst均与表达式链表对应,表明其中每一个操作符的操作符类、执行的策略(ASC或DEC)以及空值排序策略。

在初始化过程中,会使用Mergejoin构造MergeJoinState结构:

typedef struct MergeJoinState
{
JoinState js; /* its first field is NodeTag */
int mj_NumClauses;
MergeJoinClause mj_Clauses; /* array of length mj_NumClauses */
int mj_JoinState;
bool mj_ExtraMarks;
bool mj_ConstFalseJoin;
bool mj_FillOuter;
bool mj_FillInner;
bool mj_MatchedOuter;
bool mj_MatchedInner;
TupleTableSlot *mj_OuterTupleSlot;
TupleTableSlot *mj_InnerTupleSlot;
TupleTableSlot *mj_MarkedTupleSlot;
TupleTableSlot *mj_NullOuterTupleSlot;
TupleTableSlot *mj_NullInnerTupleSlot;
ExprContext *mj_OuterEContext;
ExprContext *mj_InnerEContext;
} MergeJoinState;

通过对于连接类型的判断来设置如下几个变量的值:

1)mj_FillOuter:为true表示不能忽略没有匹配项的左子节点元组,需要与空元组进行连接,在 LEFT JOIN、ANTI JOIN 和 FULL JOIN时为true。

2)mj_FillInner:为true表示不能忽略没有匹配项的右子节点元组,需要与空元组进行连接,在 RIGHT JOIN、FULL JOIN 时为 true。

3)mj_InnerTupleSlot:为右子节点元组生成的空元组,在mj_FillOuter为真时构造。

4)mj_OuterTupleSlot:为左子节点元组生成的空元组,在mj_FillInner为真时构造。

除此之外,需要将标记当前左(右)子节点元组是否找到能够连接的元组的变量mj_MatchedOuter(mj_MatchedInner)设置为 false,将存储左(右)子节点元组的字段mj_NullOuterTupleSlot(mj_InnerTupleSlot)设置为 NULL,并为mj_MarkedTupleSlot分配存储空间。


还剩一个hashjoin,我看了半天看不太懂,下篇再说吧~

跟我一起读postgresql源码(十三)——Executor(查询执行模块之——Join节点(上))的更多相关文章

  1. 跟我一起读postgresql源码(九)——Executor(查询执行模块之——Scan节点(上))

    从前面介绍的可优化语句处理相关的背景知识.实现思想和执行流程,不难发现可优化语句执行的核心内容是对于各种计划节点的处理,由于使用了节点表示.递归调用.统一接口等设计,计划节点的功能相对独立.代码总体流 ...

  2. 跟我一起读postgresql源码(十一)——Executor(查询执行模块之——Materialization节点(上))

    物化节点 顾名思义,物化节点是一类可缓存元组的节点.在执行过程中,很多扩展的物理操作符需要首先获取所有的元组后才能进行操作(例如聚集函数操作.没有索引辅助的排序等),这时要用物化节点将元组缓存起来.下 ...

  3. 跟我一起读postgresql源码(十)——Executor(查询执行模块之——Scan节点(下))

    接前文跟我一起读postgresql源码(九)--Executor(查询执行模块之--Scan节点(上)) ,本篇把剩下的七个Scan节点结束掉. T_SubqueryScanState, T_Fun ...

  4. 跟我一起读postgresql源码(八)——Executor(查询执行模块之——可优化语句的执行)

    2.可优化语句的执行 可优化语句的共同特点是它们被查询编译器处理后都会生成査询计划树,这一类语句由执行器(Executor)处理.该模块对外提供了三个接口: ExecutorStart.Executo ...

  5. 跟我一起读postgresql源码(七)——Executor(查询执行模块之——数据定义语句的执行)

    1.数据定义语句的执行 数据定义语句(也就是之前我提到的非可优化语句)是一类用于定义数据模式.函数等的功能性语句.不同于元组增删査改的操作,其处理方式是为每一种类型的描述语句调用相应的处理函数. 数据 ...

  6. 跟我一起读postgresql源码(六)——Executor(查询执行模块之——查询执行策略)

    时光荏苒,岁月如梭.楼主已经很久没有更新了.之前说好的一周一更的没有做到.实在是事出有因,没能静下心来好好看代码.当然这不能作为我不更新的理由,时间挤挤还是有的,拖了这么久,该再写点东西了,不然人就怠 ...

  7. 跟我一起读postgresql源码(五)——Planer(查询规划模块)(下)

    上一篇我们介绍了查询规划模块的总体流程和预处理部分的源码.查询规划模块再执行完预处理之后,可以进入正式的查询规划处理流程了. 查询规划的主要工作由grouping_planner函数完成.在具体实现的 ...

  8. 跟我一起读postgresql源码(四)——Planer(查询规划模块)(上)

    时间一晃周末就过完了,时间过得太快,不由得让人倍加珍惜.时间真是不够用哈~ 好的不废话,这次我们开始看查询规划模块的源码吧. 查询规划部分的在整个查询处理模块应该是在一个非常重要的地位上,这一步直接决 ...

  9. 跟我一起读postgresql源码(三)——Rewrite(查询重写模块)

    上一篇博文我们阅读了postgresql中查询分析模块的源码.查询分析模块对前台送来的命令进行词法分析.语法分析和语义分析后获得对应的查询树(Query).在获得查询树之后,程序开始对查询树进行查询重 ...

随机推荐

  1. Linux(CentOS6.5)下编译安装Nginx1.10.1

    首先在特权账号(root)下安装编译时依赖项: yum install gcc gcc-c++ perl -y   首先以非特权账号(本文以账号comex为例)登陆OS: 进入data目录下载相关安装 ...

  2. 使用trim_galore软件遇到的问题

    我的原始测序数据是双端测序,在用trim_galore软件去接头的这一步,使用的命令行是 time nohup trim_galore R17002628-SKOV3-m6A_combined_R1. ...

  3. 理解JavaScript原型

    Javascript原型总会给人产生一些困惑,无论是经验丰富的专家,还是作者自己也时常表现出对这个概念某些有限的理解,我认为这样的困惑在我们一开始接触原型时就已经产生了,它们常常和new.constr ...

  4. Python day 6(3) Python 函数式编程1

    一:函数式编程概念 函数是Python内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计.函数就是面向过程的 ...

  5. JAVA学习知识点汇总

    异常处理 1.在异常处理中,如果子类覆盖了父类的一个方法,子类方法中声明的已检查异常不能比父类方法声明的异常更通用,即子类方法可以抛出更特定的异常(不超过父类方法声明的异常范围,即由父类方法声明异常派 ...

  6. RobotFrame连接MySql数据库

    RobotFrame连接MySql数据库这类的教程网上并不多,就算有,也是很多坑.小编今天为大家提供一个靠谱的教程,但是具体的包需要大家自己下载.废话不多说,看疗效~~~ 1.pip install ...

  7. QCanvasItem介绍-QT3

    QCanvasItem类提供一个在QCanvas上的抽象图形对象. 各种QCanvasItem子类提供立即可用的行为.这个类是一个纯粹的抽象超类,它提供了在所有具体的canvas项目类中共享的行为.Q ...

  8. H5前端性能测试总结

    测试关注指标 Http请求个数 同一个域名不同浏览器内核.不同版本浏览器,大部分并发请求数是6个: 优化方案: a.雪碧图:即CSS Sprite,也称CSS精灵,是一种CSS图像合并技术,该方法是将 ...

  9. Visual Studio 中添加SQLite数据源

    相关下载:https://system.data.sqlite.org/index.html/doc/trunk/www/downloads.wiki 在Visual Studio中要支持访问SQLi ...

  10. C/C++知识点清单02-上

    第二章 预处理.const.static与sizeof (上) 在这章编写时,发现了一个不错的帖子.其中对程序预处理语句的基本概念写得还是十分清晰的. (http://www.runoob.com/c ...