当前位置:Gxlcms > 数据库问题 > 跟我一起读postgresql源码(十六)——Executor(查询执行模块之——control节点(下))

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

时间:2021-07-01 10:21:17 帮助过:11人阅读

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)处理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节点到此结束。

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

标签:select   引用   node   text   pen   apply   gre   first   exec   

人气教程排行