UPSERT功能的详细表述为当执行INSERT操作时,如果当前表中存在冲突行则更新冲突行否则插入新行,在HE3DB中通过ON CONFLICT DO UPDATE SET或者ON DUPLICATE KEY UPDATE实现,类似的功能还有REPLACE INTO。
其中,REPLACE INTO功能区别于UPSERT的点在于:
①REPLACE INTO功能为执行INSERT操作时,如果当前表存在冲突行则先删除冲突行然后再插入新行,可拆分为DELETE + INSERT操作;
②对于未指定在targetlist中的列(尤其是sequence列),REPLACE INTO会更新sequence列为最新序列,其他列为默认值,而UPSERT则不会更新sequence列,其他列为表中旧值,只更新指定列。
对于普通INSERT语法和UPSERT语法,在内核中都是通过ExecInsert函数去执行数据的插入或者更新,而投影信息构建与投影步骤执行的区别就是导致行为结果不同的原因之一,本文接下来将从源码层面解读HE3DB投影信息构建流程,投影步骤执行流程会放在下一篇文章中解读。
一、主要调用栈
投影信息构建的主要调用栈:
|->standard_ExecutorStart
-|->InitPlan
--|->ExecInitNode
---|->ExecInitModifyTable/*ModifyTable节点初始化 */
----|->ExecInitNode/递归初始化以ModifyTable节点左孩子为根节点的计划树的节点/
-----|->ExecInitResult
------|->ExecAssignProjectionInfo
-------|->ExecBuildProjectionInfo/根据targetlist构建投影(Project)信息,构建投影信息的 时候构建表达式解析步骤/
----|ExecBuildUpdateProjection/对于ONCONFLICT_UPDATE的情况,传入 node->onConflictSet构建需要UPSERT的列的投影信息,将结果保存 在ModifyTableState->resultRelInfo->ri_onConflict->oc_ProjInfo中/
二、代码解析
以postgresql15.5代码为例进行代码解析:
ExecBuildProjectionInfo:
主要步骤为:
1、初始化ProjectionInfo、ExprState,集成ExprState到ProjectionInfo。
2、调用ExecCreateExprSetupSteps函数为 SQL 表达式(如 age + 10 )生成执行前的准备步骤;内部会调用expr_setup_walker检查表达式树,记录需要的初始化工作,如记录下需要用到的字段或子查询,然后调用ExecPushExprSetupSteps根据 expr_setup_walker 提供的清单,生成具体的初始化指令,如赋值EEOP_INNER_FETCHSOME表示从内表(如 JOIN 的左表)取值,赋值EEOP_OUTER_FETCHSOME表示从从外表(如 JOIN 的右表)取值,赋值EEOP_SCAN_FETCHSOME表示从当前扫描表取值。
3、遍历targetList,处理targetList中的每一个列(下面详细说)。
- 压入EEOP_DONE步骤,表示结束表达式执行。
主要步骤:
- 判断当前列是否是安全列,以sequence列为例,*tle->expr的类型为T_NextValuesExpr非Var类型,不是安全列,"Safe"指的是在计划启动执行时不需要应用CheckVarSlotCompatibility()。
2、对于SafeVar则只需要生成EEOP_ASSIGNVAR步骤,表示直接赋值内表、外表或扫描结果到投影结果,对于属性编号小于输入的属性个数的情况,如果用户属性已被清除或者有类型不匹配,则不使用ASSIGN_VAR,而是让常规的表达式匹配处理。
3、对于非SafeVar,使用常规办法处理列表达式,调用ExecInitExprRec函数处理。
ExecInitExprRec是HE3DB 将SQL表达式的抽象树转换为可执行指令的核心函数,每个 case 处理一种表达式类型(如字段、常量、函数、逻辑运算等),生成对应的指令,确保表达式解析时能正确计算结果,如sequence列在ExecInitExprRec函数中会将opcode设为EEOP_NEXTVALUEEXPR,表示调用 ExecEvalNextValueExpr获取序列值,在投影步骤执行时会根据opcode即步骤枚举值做相应投影操作,再调用ExprEvalPushStep压步骤,然后紧接着给下一个opcode赋值EEOP_ASSIGN_TMP,表示赋值临时值到结果。
ExprEvalPushStep是HE3DB查询执行引擎中用于将单个表达式执行步骤(ExprEvalStep)添加到 ExprState 的步骤列表(steps)的函数,它的作用可以理解为把一条具体的执行指令(如“从表中取 age 值”)写入一个“执行计划笔记本”,它会检查当前“笔记本”是否有空间,如果没有就初始化或扩展空间,然后将指令写入,确保表达式的执行步骤按顺序保存,确保ExecInitExprRec等函数生成的所有指令都能被正确记录供后续查询执行使用。
5、将targetList处理完后,可以在gdb中p state->steps[0]查看第一个投影步骤的opcode,枚举值对应的步骤类型可在ExprEvalOp结构体中查找,类似的可以查看完整的投影步骤。
ExecBuildUpdateProjection:
主要步骤:
- 先构建在node->onConflictSet中的列的投影信息,如ON CONFLICT DO UPDATE SET (a,b) = (1,2)语句,则targetList包括a,b两列,evalTargetList为传入参数为true,则调用ExecInitExprRec函数处理,并通过ExprEvalPushStep压步骤。
- 然后构建不在node->onConflictSet中的剩余列,通过bms_is_member函数判断是否为剩余列,是的话则将opcode设为EEOP_ASSIGN_SCAN_VAR,后续在执行中通过scanSlot获取值。
三、总结
通过上述步骤构建出了普通INSERT操作和UPSERT操作(即node->onConflictAction=ONCONFLICT_UPDATE的操作)的投影信息,可以看到对于UPSERT操作,额外构建了一次投影信息存到了ModifyTableState->resultRelInfo->ri_onConflict->oc_ProjInfo中,这是在投影信息构建时的主要区别。