5.ModifyTable 节点
先看一个 ModifyTable 节点的例子:
- postgres=# explain update test_01 set id = 5 where name = 'xxx';
- QUERY PLAN
- ---------------------------------------------------------------
- Update on test_01 (cost=0.00..23.75 rows=6 width=48)
- -> Seq Scan on test_01 (cost=0.00..23.75 rows=6 width=48)
- Filter: ((name)::text = 'xxx'::text)
- (3 rows)
你可能疑惑为啥上面的查询计划里面没有 "ModifyTable" 这样的字眼, 下面是 explain.c 文件中的一段:
- case T_ModifyTable:
- sname = "ModifyTable";
- switch (((ModifyTable * ) plan) - >operation) {
- case CMD_INSERT:
- pname = operation = "Insert";
- break;
- case CMD_UPDATE:
- pname = operation = "Update";
- break;
- case CMD_DELETE:
- pname = operation = "Delete";
- break;
- default:
- pname = "???";
- break;
- }
- break;
由此我们可以看到, 对于 ModifyTable 节点, explain 会判断是增删改中的哪一种从而作出判断所以当在 explain 中看到 INSERTUpdate 和 Delete 时, 我们就知道这是走了 ModifyTable 节点
那这里, 我们还要再解释 ModifyTable 节点么? 解释下吧:
- * Apply rows produced by subplan(s) to result table(s),
- * by inserting, updating, or deleting.
也就是说, 我先从下层的 subplan 中获得 rows, 然后根据命令类型选择是 insert, update 还是 delete 操作所以我们可以知道, 这是一个顶层节点, 它下面是查询节点 (就是 SELECT) 这也符合我们以前一直说的, 所有的增删改查其实都是 SELECT!
- typedef struct ModifyTable
- {
- Plan plan;
- CmdType operation; /* INSERT, UPDATE, or DELETE */
- bool canSetTag; /* do we set the command tag/es_processed? */
- Index nominalRelation; /* Parent RT index for use of EXPLAIN */
- List *resultRelations; /* integer list of RT indexes */
- int resultRelIndex; /* index of first resultRel in plan's list */
- List *plans; /* plan(s) producing source data */
- List *withCheckOptionLists; /* per-target-table WCO lists */
- List *returningLists; /* per-target-table RETURNING tlists */
- List *fdwPrivLists; /* per-target-table FDW private data lists */
- List *rowMarks; /* PlanRowMarks (non-locking only) */
- int epqParam; /* ID of Param for EvalPlanQual re-eval */
- OnConflictAction onConflictAction; /* ON CONFLICT action */
- List *arbiterIndexes; /* List of ON CONFLICT arbiter index OIDs */
- List *onConflictSet; /* SET for INSERT ON CONFLICT DO UPDATE */
- Node *onConflictWhere; /* WHERE for ON CONFLICT UPDATE */
- Index exclRelRTI; /* RTI of the EXCLUDED pseudo relation */
- List *exclRelTlist; /* tlist of the EXCLUDED pseudo relation */
- } ModifyTable;
由于 ModifyTable 节点涉及的操作比较多, 这里稍微解释下 ModifyTable 中的一些字段
withCheckOptionLists 字段
这个和视图相关, 我们知道创建视图有这样的用法:
CREATE VIEW xxx_view AS query WITH CHECK OPTION
在 postgres 中, 对创建语句中带有 WITH CHECK OPTION 的 VIEW, 在通过视图进行的操作(增删改), 必须也能通过该视图看到操作后的结果
也就是说:
对于 INSERT, 那么加的这条记录在视图查询后必须可以看到
对于 UPDATE, 修改完的结果也必须能通过该视图看到
对于 DELETE, 只能删除视图里有显示的记录
因此对这一类操作, 我们在操作表 / 视图的时候, 要在(INSERT/UPDATE/DELETE 的)WHERE 条件中加上 WITH OPTION 中的条件
returningLists 字段
这个很简单, 因为 postgres 的语法中有类似以下的用法:
- DELETE FROM xxx_table WHERE condition RETURNING xxx;
- UPDATE xxx_table SET a = '123'WHERE condition RETURNING xxx;
- INSERT INTO xxx_table VALUES(somevalues) RETURNING xxx;
是的, postgres 的 RETURNING 子句可以返回修改的行, 所以对于含有 RETURNING 子句的查询, 除了在对表中的数据进行 INSERT/UPDATE/DELETE, 还要额外返回一些行即还要有额外的输出
fdwPrivLists 字段
Postgres 支持访问外部数据库的嘛, 所以这个字段提供对 fdw 的处理的支持
rowMarks 字段
这个和行锁相关, 针对 SELECT 的 LOCK 子句:
FOR lock_strength[OF table_name[, ...]][NOWAIT | SKIP LOCKED]
具体见这里: http://www.postgres.cn/docs/9.5/sql-select.html
onConflictActionarbiterIndexesarbiterIndexes 和 onConflictWhere 字段
是的, 对于 INSERT 操作, 我们有以下语法(用于支持 INSERT 中发生的冲突):
INSERT INTO table_name VALUES (somevalues) ON CONFLICT [ conflict_target ] conflict_action
并且 conflict_action 是以下之一:
- DO NOTHING
- DO UPDATE SET { column_name = { expression | DEFAULT } |
- ( column_name [, ...] ) = ( { expression | DEFAULT } [, ...] ) |
- ( column_name [, ...] ) = ( sub-SELECT )
- } [, ...]
- [ WHERE condition ]
这样一看, 很容易对的上了
exclRelRTIexclRelTlist 字段
对于建表语句有以下子句:
EXCLUDE[USING index_method](exclude_element WITH operator[, ...]) index_parameters[WHERE(predicate)]
EXCLUDE 子句指定一个排除约束, 它保证如果任意两行在指定列或表达式上使用指定操作符进行比较, 不是所有的比较都将会返回 TRUE 具体见这里:
因此, 你可以把这个字段看做是一个约束字段, 在做更新操作时需要判断
- typedef struct ModifyTableState
- {
- PlanState ps; /* its first field is NodeTag */
- CmdType operation; /* INSERT, UPDATE, or DELETE */
- bool canSetTag; /* do we set the command tag/es_processed? */
- bool mt_done; /* are we done? */
- PlanState **mt_plans; /* subplans (one per target rel) */
- int mt_nplans; /* number of plans in the array */
- int mt_whichplan; /* which one is being executed (0..n-1) */
- ResultRelInfo *resultRelInfo; /* per-subplan target relations */
- List **mt_arowmarks; /* per-subplan ExecAuxRowMark lists */
- EPQState mt_epqstate; /* for evaluating EvalPlanQual rechecks */
- bool fireBSTriggers; /* do we need to fire stmt triggers? */
- OnConflictAction mt_onconflict; /* ON CONFLICT type */
- List *mt_arbiterindexes; /* unique index OIDs to arbitrate
- * taking alt path */
- TupleTableSlot *mt_existing; /* slot to store existing target tuple in */
- List *mt_excludedtlist; /* the excluded pseudo relation's
- * tlist */
- TupleTableSlot *mt_conflproj; /* CONFLICT ... SET ... projection
- * target */
- } ModifyTableState;
那么对于 ModifyTableState 的一些字段, 我们参照 ModifyTable 节点的解释, 也能理解的差不多, 这里不多说了
下面进入正题:
ModifyTable 节点的初始化由 ExecInitModifyTable 函数来做该函数除了做一些基础的初始化操作外, 针对我上面提到的那些字段, 做了设置和初始化说细一点的话就是:
(1)调用 ExecInitNode 函数对 ModifyTable 节点中的 plans 列表中的 subplans 节点进行初始化并将其结果保存到 ModifyTableState 结构的 mt_plans 字段中在这一步中, 同时也顺便做了这些事: 验证了查询所涉及的 target relations 是否是合法的; 打开这些 target relations 上的 index, 因为对于 UPDATE/INSERT 操作, 我们同时还要对相应的索引进行操作(DELETE 操作不删除索引, DELETE 后遗留的 index 留给 VACUUM 去清理)
(2)根据 ModifyTable 节点中的 withCheckOptionLists 字段初始化上面提到的 WITH CHECK OPTION(如果存在的话)初始化后保存在 ModifyTableState 结构的 resultRelInfo 字段的成员变量 ri_WithCheckOptions 和 ri_WithCheckOptionExprs 中
(3)根据 ModifyTable 节点中的 returningLists 字段初始化上面提到的 RETURNING 子句 (如果存在的话) 并根据此构造返回的结果集的类型如果 returningLists 字段为空, 说明没有 RETURNING 子句那么返回结果集的类型设置为 NULL
(4)如果存在 ON CONFLICT DO UPDATE 字段那么为他初始化目标列表 Target List 投影条件 resultRelInfo 和过滤条件 qual, 结果保存在 ModifyTableState 结构的相应字段中
(5)处理 ModifyTable 节点中的 RowMark 字段, 结果保存在 ModifyTableState 结构的 mt_arowmarks 字段中
(6)初始化 junk filter 这个 junk filter 的由来是因为在 plan 阶段, 我们会产生一些 "中间信息" 放在 tuple 中供 Excutor 使用比如 ctid, 用来定位该元组放到磁盘文件的位置但是当我们将元组写到磁盘时, 我们不需要保存这个信息那么这个信息相当于是冗余的了, 我们需要用这个 JunkFilter 将其过滤和删除
(7)我们知道我们在 INSERT/UPDATE/DELETE 时可能会涉及到 trigger, 这里设置 trigger 相关的 slot, 供后续函数调用
(8)如果本 ModifyTable 节点不是顶层 ModifyTable 节点的话(上层还有 ModifyTable 节点), 设置全局状态结构 estate->es_auxmodifytables 属性, 做上标记
ModifyTable 节点的执行由 ExecModifyTable 函数执行具体的来说:
(1)首先我们要记得可能有 BEFORE STATEMENT triggers 这玩意儿, 顾名思义, 就是要在 STATEMENT 执行之前执行这个 trigger 如果存在, 在进入正式的处理之前我们先要调用 fireBSTriggers 函数来处理它
(2)接下来是一个大 for 循环在这个 for 循环里面, 程序调用 ExecProcNode 函数循环地从下层节点中读取元组需要注意的是这个循环里面类似 Append 节点的操作, 在读取完第一个 subplans 节点中的元组后, 会依次读取后续 subplan 中的元组, 直到全部读取完毕我们以前说过 postgres 是一次读取一个元组并处理一个元组的这里也不例外, 每读取一个元组后根据操作的类型分别调用 ExecInsert/ExecUpdate/ExecDelete 函数去处理
(3)有始有终, 既然可能有 BEFORE STATEMENT triggers, 那么也可能有 AFTER STATEMENT triggers, 这里调用 fireASTriggers 函数来处理它
那么我们应该对 ExecInsert/ExecUpdate/ExecDelete 函数感兴趣了下面我们开始讨论他们
1.ExecInsert
对于 ExecInsert 函数的话, 主要是两件事: 将元组插入到目标表 target relation 中同时将对应的索引项插入到相关的索引表 index relations(可能有多个索引要处理)中
(1)首先要将需要插入的 tuple 从 slot 中取出, 本地化 why? 因为这个 slot 在随后的操作 heap_insert 函数中可能不安全, 因此将其提前取出来这个工作由 ExecMaterializeSlot 函数完成
(2)从全局状态中 estate->es_result_relation_info 获取信息, 判断 result relation 是否需要一个 oid 如果需要, 则先将 tuple 中的 oid 字段设为 0
(3)处理 BEFORE ROW INSERT Triggers 这里我们要注意这个触发器是 ROW 级别的, 而 BEFORE STATEMENT triggers 是语句级别的, 他们不一样
(4)处理 INSTEAD OF ROW INSERT Triggers 如果存在则调用 ExecIRInsertTriggers 函数去处理并直接返回, 不进行 INSERT 操作
(5)处理 foreign table 的情况, 为其初始化 ri_FdwRoutine 调用 foreign server 的 API 去处理该条元组的插入并获取返回的 slot
(6)处理 WITH CHECK OPTION 中的条件 (ExecWithCheckOptions 函数) 和唯一性约束(ExecConstraints 函数)ON ONCONFLICT OPTION
(7)如果存在 ON ONCONFLICT OPTION 条件, 则先获得 speculative insertion lock, 调用 heap_insert 函数将元组插入到堆表中如果插入成功, 不发生冲突则正常释放该 lock 否则强制释放 lock, 并执行 ON ONCONFLICT DO UPDATE(如果有的话)
(8)不存在 (7) 中的条件, 我们正常地调用 heap_insert 函数将元组插入到堆表中同时调用 ExecInsertIndexTuples 函数插入相应的索引元组
(9)调用 ExecARInsertTriggers 函数处理 AFTER ROW INSERT Triggers 类似 (3) 的处理
(10)还记得上面提到的 CREATE VIEW 中的 WITH CHECK OPTION 么? 这里调用 ExecWithCheckOptions 函数做处理, 不满足则报错退出
(11)如果存在 RETURNING 子句, 我们调用 ExecProcessReturning 函数处理之
2.ExecDelete
ExecDelete 函数相对简单, 他只需要将元组删除即可, 不需要针对索引做任何操作
(1)从全局状态中 estate->es_result_relation_info 获取信息
(2)处理 BEFORE ROW DELETE Triggers 这里我们要注意这个触发器是 ROW 级别的, 而 BEFORE STATEMENT triggers 是语句级别的, 他们不一样
(3)处理 INSTEAD OF ROW DELETE Triggers 如果存在则调用 ExecIRDeleteTriggers 函数去处理并直接返回, 不进行 INSERT 操作
(4)处理 foreign table 的情况, 为其初始化 ri_FdwRoutine 调用 foreign server 的 API 去处理该条元组的删除并获取返回的 slot
(5)我们正常地调用 heap_delete 函数执行 DELETE 操作如果返回值不是 HeapTupleMayBeUpdated 则说明操作失败, 根据失败的错误代码执行相应的处理
(6)调用 ExecARDeleteTriggers 函数处理 AFTER ROW DELETE Triggers 类似 (2) 的处理
(7)如果存在 RETURNING 子句, 我们调用 ExecProcessReturning 函数处理之
3.ExecUpdate
ExecUpdate 函数实际上执行的是 "INSERT" 操作因为 postgres 内部是 MVCC 机制, 多版本并发控制旧的元组实际上没有删除, 只是不再引用同时, UPDATE 操作在数据库内部也是要在 "transaction" 中的, 否则 postgres 会不停的将新增的 updated 元组看成是需要 update 的元组, 循环下去
(1)判断当前是否属于 BootstrapProcessing 模式, 在该模式下所有的 transaction id 都被设置为 1 这个时候才能保证不循环更新
(2)首先要将需要插入的 tuple 从 slot 中取出, 本地化 why? 因为这个 slot 在随后的操作 heap_update 函数中可能不安全, 因此将其提前取出来这个工作由 ExecMaterializeSlot 函数完成
(3)处理 BEFORE ROW UPDATE Triggers
(4)处理 INSTEAD OF ROW UPDATE Triggers 如果存在则调用 ExecIRUpdateTriggers 函数去处理并直接返回, 不进行 INSERT 操作
(5)处理 foreign table 的情况, 为其初始化 ri_FdwRoutine 调用 foreign server 的 API 去处理该条元组的更新并获取返回的 slot
(6)处理 WITH CHECK OPTION 中的条件 (ExecWithCheckOptions 函数) 和唯一性约束(ExecConstraints 函数)ON ONCONFLICT OPTION
(7)我们正常地调用 heap_update 函数执行 UPDATE 操作如果返回值不是 HeapTupleMayBeUpdated 则说明操作失败, 根据失败的错误代码执行相应的处理如果成功, 则调用 ExecInsertIndexTuples 函数向索引中插入索引元组
(8)调用 ExecARUpdateTriggers 函数处理 AFTER ROW UPDATE Triggers
(9)针对表的上层 VIEW 再次执行 WITH CHECK OPTION
(10)如果存在 RETURNING 子句, 我们调用 ExecProcessReturning 函数处理之
ModifyTable 节点的清理简单些了 (ExecEndModifyTable 函数) 除了常规的清理工作, 清理可能存在 FDW 结构, 清理初始化中额外初始化的那些 subplans 节点
control 节点到此结束
来源: https://www.cnblogs.com/flying-tiger/p/8418293.html