MergeImplementationDetails
本页详细介绍了 MERGE 命令的实现,该实现是在 GSoC 2010 期间开发的。
此命令从未集成到 PostgreSQL 中,要达到可用于生产的质量,还需要大量的修改。
实现大纲
要将 MERGE 命令添加到 PostgreSQL 系统中,我们需要建立一个完整的工作流程来实现这种新型查询。我们修改的部分包括
1. 解析器:添加 MERGE 命令的语法定义,将用户查询字符串转换成内部结构。
2. 分析器:对 MERGE 命令语句进行语义检查,并为其构建一个“查询”节点。
3. 重写器:重写 MERGE 查询及其操作。必要时触发规则。
4. 计划器:构建计划树。
5. 执行器:运行计划
此外,我们需要修改 EXPLAIN 命令,以使 MERGE 查询易于解释。
解析器
数据结构
我们为 MERGE 命令在解析器中创建了两个节点。
/*The structure for MERGE command statement*/ typedef struct MergeStmt { NodeTag type; RangeVar *relation; /*targe relation for merge */ /* source relations for the merge. *Currently, we only allwo single-source merge, *so the length of this list should always be 1 */ List *source; Node *matchCondition; /* qualifications of the merge*/ /*list of MergeConditionAction structure. *It stores all the matched / not-matched conditions and the corresponding actions *The elments of this list are MergeConditionAction nodes */ List *actions; }MergeStmt;
/* the structure for the actions of MERGE command. * Holds info of the clauses like "WHEN MATCHED/NOT MATCHED AND <qual> THEN UPDATE/DELETE/INSERT" */ typedef struct MergeConditionAction { NodeTag type; bool match; /*match or not match*/ Node *condition;/*the AND condition for this action*/ Node *action; /*the actions: delete , insert or update*/ }MergeConditionAction;
MERGE 命令的信息将经过解析,转换成一个“MergeStmt”结构。“MergeStmt”中的“actions”字段是“MergeConditionAction”结构的列表。
一个“MergeConditionAction”代表命令中的一个动作。
此外,我们在 kwlist.h 中添加了两个新的关键词,MERGE 和 MATCHED
PG_KEYWORD("matched", MATCHED, UNRESERVED_KEYWORD) PG_KEYWORD("merge", MERGE, UNRESERVED_KEYWORD)
过程
我们在 /parser/gram.y 中添加了 MERGE 命令的定义。
目标关系名称、源表(可以是带有别名的子查询)和匹配条件都可以由 gram.y 中的现有组件表示
合并动作子句(WHEN … 子句)将转换成 MergeConditionAction 节点,并附加在“actions”列表中。
对于“匹配”操作,布尔字段“匹配”将被设为真。操作可以指定自己的其他限定条件,该限定条件是记录在“condition”字段中。操作的其余部分(例如更新操作的“SET …”从句或插入操作的“VALUES …”从句)将被转换成“MergeUpdate/MergeInsert/MergeDelete”节点,并放入“MergeConditionAction”的“action”字段中。实际上这些节点是,也为它们创建了新的节点标签以示区别。
分析器
数据结构
一条 MERGE 命令需要为其操作创建一系列其他查询(非子查询)。合并操作的查询节点与其公用 UPDATE/INSERT/DELETE 查询颇为相似。为了保存合并操作查询并将它们与其他查询区分开来,我们在“Query”结构中添加了两个字段。
typedef struct Query { NodeTag type; CmdType commandType; /* select|insert|update|delete|utility */ List *rtable; /* list of range table entries */ ……… bool isMergeAction; /*if this query is a merge action. */ List *mergeActQry; /* the list of all the merge actions. * used only for merge query statment*/ } Query;
对于合并命令,我们将为其主查询(将源表与目标表联接的查询)创建一个“Query”节点。并且合并操作将被转换成“Query”节点,其“isMergeAction”字段被设为真。然后,这些合并操作的“Query”节点将被链接到其主查询的“mergeActQry”字段。
注意
此处的一个特殊之处在于所有合并操作查询共用相同的范围表列表。范围表是在转换主查询时创建的。并且合并操作的转换不会创建自己的范围表,而只是让自己的“rtable”直接指向主查询的范围表。
过程
transformMergeStmt()
解析器创建的“MergeStmt”将发送到/parser/analyze.c 中“transformStmt()”函数。我们在该函数中添加一个新的 switch case,并向函数“transformMergeStmt()”传递该节点。
在该函数中,我们以以下方式构建 MERGE 命令的主查询
1. 创建一个空白的“Query”节点,其命令类型为“CMD_MERGE”。
2. 使用“RowExclusiveLock”打开目标表的关系。尚未将其添加到范围表中。
3. 使用源表和目标表分别作为左参数和右参数制作一个“JoinExpr”节点。联接类型为左联接。此联接表达式将成为主查询“来源列表”中的唯一元素。
4. 制作只有一个“A_Start”引用的目标列表。
5. 将此查询转换为一个常规的 SELECT 查询。因此,我们将获得一个类似于命令的查询
SELECT * FROM <source_table> LEFT JOIN <target_table> ON <match_condition>
6. 找到目标表的范围表项,将其设为该查询真正的“resultRelation”。
7. 对于每个合并操作,使用“transformMergeActions()”对其进行处理,并将返回的结果追加到主查询的“mergeActQry”列表中。
8. 返回主查询作为最终结果。
transformMergeActions()
对于转换合并操作,我们需要执行以下操作
1. 对操作进行简单的检查。确保仅对“匹配”案例采取插入操作,而对“不匹配”案例采用更新/删除操作。
2. 将目标关联引用和其他限定条件放入 MergeUpdate/MergeInsert/MergeDelete 节点的相应字段中。然后将其转换为具有功能“transformStmt()”的常见节点。
3. 返回结果。请注意,所有合并操作查询都与主查询共享完全相同的范围表,但它们具有自己的目标列表和连接树条件。
注意
1. 在“transformMergActions()”中,ParseState直接继承自主查询进程。因此,其中的范围表与主查询中的范围表是同一个。
2. “transformStmt()”将节点传递给相应的功能(例如“transformUpdateStmt()”)。但是我们已经修改了这些函数,以便它们不会接触到范围表。
3. 插入操作查询与常见的插入查询稍有不同。
在普通的插入中,值列表只能包含常量和 DEFAULT(我说的对吗?)但是,在 MERGE 命令的插入操作中,值列表中可以有包含变量(目标表和源表的属性)的表达式。
此外,在普通的插入中,值列表后面永远不会跟着 WHERE 子句。但是在 MERGE 插入操作中,存在匹配条件。因此,此函数的输出查询是“INSERT...VALUES...“样式的插入查询,除了我们有其他范围表和 WHERE 子句之外。
请注意,它还不同于“INSERT ... SELECT...”查询,其中整个 SELECT 是一个子查询(我们这里没有子查询)。
重写程序
对于修改表查询的重写程序,其主要工作是根据查询的结果关联上定义的规则生成额外的查询。将在用户输入查询之前或之后执行规则生成的查询。
MERGE 命令在重写程序中的过程很简单。我们需要针对每个合并操作触发规则,并汇总所有操作的规则生成查询。我们通过在 /rewrite/rewriteHandler.c 中的函数“QueryRewrite()”中添加代码来完成此操作
由于合并操作本身就是“Query”节点,因此它们可以直接应用于“RewriteQuery()”。
我们只需要确保
1. 所有的操作查询都应该由“QueryRewrite()”。但是,同类型的操作不应该多次生成规则查询。
2. 如果一个操作被 INSTEAD 规则(或 NOTHING)替换,则它应该连同其他所有同类型操作一起从操作列表中移除。
3. 如果合并命令的所有合并操作都被 INSTEAD 规则替换,则 MERGE 命令查询本身也应该从查询队列中移除。
规划程序
数据结构
在规划程序中,系统为需要扫描和连接表的查询构建计划树。计划树中的节点定义从表中获取结果元组的过程中的各种操作。
对于一个表修改查询,即一个 UPDATE/INSERT/DELETE 查询,Planner 将把计划树打包到 “ModifyTable” 节点中,这是一个 PostgreSQL 9 中的新类型的计划节点。
作为新表修改查询,MERGE 也采用这种方式处理。“pg_analyze_and_rewrite()” 生成的主查询将由 “Planner()” 函数完全处理。最后作为 “ModifyTable” 节点返回。主查询的计划将放入 “ModifyTable” 节点中 List 域 “plans” 中。我们更愿意把这个计划称为“主计划”而不是 “顶级计划”。它包含用于连接源表和目标表的所有信息。但是,从技术上讲,它仍然位于合并操作计划之下,因为主计划生成的元组将由(其中之一)合并操作进一步处理。
MergeAction
我们专门设计了一个新计划类型用于合并操作,即 “MergeAction” 节点。
typedef struct MergeAction { Plan plan; CmdType operation;/* INSERT, UPDATE, or DELETE */ List *flattenedqual; /*the flattened qual expression of action*/ }MergeAction;
ModifyTable 中的 mergeActPlan
在 Planner 中为合并操作创建的 “MergeAction” 节点将打包在 “ModifyTable” 节点中。为了保存这些节点,我们在 “ModifyTable” 中添加了一个 List 域 “mergeActPlan”。因此,合并操作的节点将链接在其主计划的 “ModifyTable” 的 “mergeActPlan” 中。
typedef struct ModifyTable { Plan plan; CmdType operation; /* INSERT, UPDATE, or DELETE */ List *plans; /* plan(s) producing source data */ …… List *mergeActPlan; /*the plans for merge actions, which are also ModifyTable nodes*/ } ModifyTable;
总之,我们可以如下图所示来表示这个结构。
过程
MERGE 命令的 “Query” 节点将通过 “Planner()” 传递给函数 “subquery_planner()”,后者将以通用方式完成构建主计划的大部分主要工作。
此函数的末尾处将主计划放入一个 “ModifyTable” 节点中以便返回。在此步骤中,我们通过函数 “merge_action_planner()” 处理所有合并操作,并将结果链接到 “mergeActPlan” 列表。
merge_action_planner()
此函数根据合并操作的 “Query” 节点生成一个 “ModifyTable” 节点。它执行非常有限的任务,包括
0. 制作一个新的 “MergeAction” 节点作为返回结果。
1. 预处理目标列表
2. 预处理 Query 的 JoinTree 中的条件。
3. 为 EXPLAIN 命令生成一个 “扁平” 条件表达式。
4. 上推目标列表和条件中的 Vars。
5. 将所有信息片段放入结果 “MergeAction” 中。用 “ModifyTable” 节点打包它并返回。
第 4 步说明
准备 MERGE 命令时,我们已将源表和目标表作为一个主计划进行左联接。在这种情况下,范围表仅包含三个范围表条目
- 第一个是源表的条目,它可以是子查询或普通表
- 第二个是目标表的条目,它是普通表
- 第三个是联接表达式,其参数是源表和目标表。
这个命令的每个合并操作也有它自己的查询和计划节点。而且,它的目标列表和条件表达式中的变量可以引用上面三个范围表条目中的某个属性。
可是,由于合并操作的结果元组槽是从连接返回的元组中进行投影的,我们需要将源表和目标表的变量映射到主计划的第三范围表条目的相应属性中。
因此,我们需要一个函数来完成与“flatten_join_alias_vars()”函数相反的操作。它将遍历“MergeAction”计划的目标列表和条件,更改变量的 varno 和 varattno,使其指向主计划结果元组中的相应属性。
执行器
数据结构
由于我们有了新的计划节点 MergeAction,我们需要为它定义一个相应的计划状态节点,即“MergeActionState”。
typedef struct MergeActionState { PlanState ps; /* its first field is NodeTag */ CmdType operation; } MergeActionState;
类似地,我们在“ModifyTableState”中添加一个新列表字段“mergeActPstates”来保存这些“MergeActionState”结构。
typedef struct ModifyTableState { PlanState ps; /* its first field is NodeTag */ CmdType operation; …… List *mergeActPstates; /*the list of the planstate of meger command actions. NIL if this is not a merge command. The elements if it are MergeActionState nodes*/ } ModifyTableState;
过程
执行器有 3 个主要阶段函数:“ExecutorStart()”、“ExecutorRun()”和“ExecutorEnd()”。在新阶段中需要新代码,而结束阶段保持不变。
执行器开始
在此阶段,主要工作是初始化计划,构建查询的执行状态。这项工作是通过递归调用“ExecInitNode()”函数来完成的。
“ExecInitNode()”将遍历计划树,并将不同的计划节点传递到它们相应的初始化函数。
对于 MERGE 命令计划(以及其他修改表的查询),它将首先转到“ExecInitModifyTable()”,该函数用于“ModifyTable”计划。在这个函数的最后,我们添加了遍历输入计划的“mergeActPlan”列表的代码,并将结果收集到返回结果的“mergeActPstates”列表中。
“ExecInitMergeAction()”
在“ExecInitNode()”中,我们为 MergeAction 计划类型添加一个分支。这个分支流向函数“ExecInitMergeAction()”。在这个函数中,我们将执行以下操作
1. 创建一个用于返回的 MergeActionState 结构。
2. 初始化结果元组槽。
3. 初始化元组类型
4. 为节点创建表达式上下文
5. 初始化目标列表和条件中的子表达式
6. 初始化投影信息
最后,合并命令的初始化计划状态结构如下所示。
执行器运行
在此阶段,MERGE 命令由“ExecModifyTable()”函数执行。常规过程很简单。
首先,执行主计划,并且它的元组逐个返回。
然后,对于主计划生成每个槽,我们尝试从中提取“ctid”属性。如果 ctid 为 NULL,则我们得到一个 NOT MATCHED 元组。
然后插槽和 ctid 被传递到“ExecMerge()”函数。在此函数中,逐个评估合并动作。如果元组符合合并动作的要求,我们将从主计划的插槽中投影出该动作的目标元组插槽。然后,根据动作的命令类型,将所有信息传递给“ExecInsert()”、“ExecDelete()”或“ExecUpdate()”。随后,进行表修改操作。
执行一项操作后,将跳过剩余操作。
解释 MERGE 命令
EXPLAIN 命令可以显示查询的计划详细信息。我们对系统进行了必要的修改,以便使 MERGE 命令可解释。
首先,在 gram.y 中,我们将“MergeStmt”添加为 ExplainableStmt 成员之一。这样一来,psql 将接受 EXPLAIN MERGE ... 命令。
然后,在“ExplainNode()”函数中,将以其他“ModifyTable”计划的形式解释 MERGE 命令的主计划。在执行该操作之前,调用“ExplainMergeActions()”函数来显示合并动作。