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. node基础篇二:模块、路由、全局变量课堂(持续)

    今天继续更新node基础篇,今天主要内容是模块.路由和全局变量. 模块这个概念,在很多语言中都有,现在模块开发已经成为了一种潮流,它能够帮助我们节省很多的时间,当然咱们的node自然也不能缺少,看下例 ...

  2. Ajax 原生和jQuery的ajax用法

    https://www.cnblogs.com/jach/p/5709175.html form数据的序列化: $('#submit').click(function(){ $('#form').se ...

  3. swift内存管理中的引用计数

    在swift中,每一个对象都有生命周期,当生命周期结束会调用deinit()函数进行释放内存空间. 观察这一段代码: class Person{ var name: String var pet: P ...

  4. VS2015 查看类之间的继承关系

    ---恢复内容开始--- 1. 右击项目名称,单击"查看"菜单下的"查看类图"菜单: 2.生成的类图如下:

  5. MySQL数据类型转换函数CAST与CONVERT的用法

    MySQL 的CAST()和CONVERT()函数可用来获取一个类型的值,并产生另一个类型的值.两者具体的语法如下: 1.CAST(value as type) 就是CAST(xxx AS 类型) 2 ...

  6. liveshow回顾

    在2017年8月14号的一天接到一个即看即买的项目,大致功能如下 1.现场走秀直播同步到H5页面 2.实时显示直播间人数 3.点赞并实时显示给用户 4.在某个时间点,可以全体推送一些消息给所有用户 5 ...

  7. 说说那些经典的web前端面试题

    阅读目录 JavaScript部分 JQurey部分 HTML/CSS部分 正则表达式 开发及性能优化部分 本篇收录了一些面试中经常会遇到的经典面试题以及自己面试过程中遇到的一些问题,并且都给出了我在 ...

  8. css写的常见图形

    .aly-tooltip { display: inline-block; padding: 5px; padding-left: 15px; padding-right: 15px; backgro ...

  9. rjs 合并压缩完 js 后 js 不压缩的问题

    线下用 requirejs 开发完后,代码上线前要用 rjs 将多个有依赖关系的 js 文件压成一个,然后某天居然发现压成一个的 js 文件,没有压缩!!!几万行的 js!!! 很显然,是 uglif ...

  10. spring的父子容器

    在创建ssm项目工程时,经常需要读取properties资源配置文件,传统的方法当然可以. 但是spring提供了更简便的方法,@value注解. 在page.properties文件中,配置分页信息 ...