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

时间:2021-05-14 17:42:32

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中看到INSERT、Update和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



onConflictAction、arbiterIndexes、arbiterIndexes和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 ]

这样一看,很容易对的上了。



exclRelRTI、exclRelTlist字段

对于建表语句有以下子句:

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)处理RLS INSERT WITH CHECK中的条件(ExecWithCheckOptions函数,"Row-Level Security (RLS) support" 是9.5版本的主要特性之一,提供了基于行的安全策略,限制数据库用户的查看表数据权限)和唯一性约束(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)处理RLS UPDATE WITH CHECK中的条件(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节点到此结束。