From a1115fa0782378a8238045d238ae70cac36be8ae Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Tue, 6 Apr 2021 18:13:05 -0400 Subject: [PATCH] Postpone some more stuff out of ExecInitModifyTable. Delay creation of the projections for INSERT and UPDATE tuples until they're needed. This saves a pretty fair amount of work when only some of the partitions are actually touched. The logic associated with identifying junk columns in UPDATE/DELETE is moved to another loop, allowing removal of one loop over the target relations; but it didn't actually change at all. Extracted from a larger patch, which seemed to me to be too messy to push in one commit. Amit Langote, reviewed at different times by Heikki Linnakangas and myself Discussion: https://postgr.es/m/CA+HiwqG7ZruBmmih3wPsBZ4s0H2EhywrnXEduckY5Hr3fWzPWA@mail.gmail.com --- src/backend/executor/execMain.c | 1 + src/backend/executor/nodeModifyTable.c | 365 ++++++++++++++----------- src/include/nodes/execnodes.h | 2 + 3 files changed, 210 insertions(+), 158 deletions(-) diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index bf43e7d379e..78ddbf95f68 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -1221,6 +1221,7 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo, resultRelInfo->ri_projectNew = NULL; resultRelInfo->ri_newTupleSlot = NULL; resultRelInfo->ri_oldTupleSlot = NULL; + resultRelInfo->ri_projectNewInfoValid = false; resultRelInfo->ri_FdwState = NULL; resultRelInfo->ri_usesFdwDirectModify = false; resultRelInfo->ri_ConstraintExprs = NULL; diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index 249555f234b..6a16752c73a 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -371,6 +371,139 @@ ExecComputeStoredGenerated(ResultRelInfo *resultRelInfo, MemoryContextSwitchTo(oldContext); } +/* + * ExecInitInsertProjection + * Do one-time initialization of projection data for INSERT tuples. + * + * INSERT queries may need a projection to filter out junk attrs in the tlist. + * + * This is "one-time" for any given result rel, but we might touch + * more than one result rel in the course of a partitioned INSERT. + * + * This is also a convenient place to verify that the + * output of an INSERT matches the target table. + */ +static void +ExecInitInsertProjection(ModifyTableState *mtstate, + ResultRelInfo *resultRelInfo) +{ + ModifyTable *node = (ModifyTable *) mtstate->ps.plan; + Plan *subplan = outerPlan(node); + EState *estate = mtstate->ps.state; + List *insertTargetList = NIL; + bool need_projection = false; + ListCell *l; + + /* Extract non-junk columns of the subplan's result tlist. */ + foreach(l, subplan->targetlist) + { + TargetEntry *tle = (TargetEntry *) lfirst(l); + + if (!tle->resjunk) + insertTargetList = lappend(insertTargetList, tle); + else + need_projection = true; + } + + /* + * The junk-free list must produce a tuple suitable for the result + * relation. + */ + ExecCheckPlanOutput(resultRelInfo->ri_RelationDesc, insertTargetList); + + /* We'll need a slot matching the table's format. */ + resultRelInfo->ri_newTupleSlot = + table_slot_create(resultRelInfo->ri_RelationDesc, + &estate->es_tupleTable); + + /* Build ProjectionInfo if needed (it probably isn't). */ + if (need_projection) + { + TupleDesc relDesc = RelationGetDescr(resultRelInfo->ri_RelationDesc); + + /* need an expression context to do the projection */ + if (mtstate->ps.ps_ExprContext == NULL) + ExecAssignExprContext(estate, &mtstate->ps); + + resultRelInfo->ri_projectNew = + ExecBuildProjectionInfo(insertTargetList, + mtstate->ps.ps_ExprContext, + resultRelInfo->ri_newTupleSlot, + &mtstate->ps, + relDesc); + } + + resultRelInfo->ri_projectNewInfoValid = true; +} + +/* + * ExecInitUpdateProjection + * Do one-time initialization of projection data for UPDATE tuples. + * + * UPDATE always needs a projection, because (1) there's always some junk + * attrs, and (2) we may need to merge values of not-updated columns from + * the old tuple into the final tuple. In UPDATE, the tuple arriving from + * the subplan contains only new values for the changed columns, plus row + * identity info in the junk attrs. + * + * This is "one-time" for any given result rel, but we might touch more than + * one result rel in the course of a partitioned UPDATE, and each one needs + * its own projection due to possible column order variation. + * + * This is also a convenient place to verify that the output of an UPDATE + * matches the target table (ExecBuildUpdateProjection does that). + */ +static void +ExecInitUpdateProjection(ModifyTableState *mtstate, + ResultRelInfo *resultRelInfo) +{ + ModifyTable *node = (ModifyTable *) mtstate->ps.plan; + Plan *subplan = outerPlan(node); + EState *estate = mtstate->ps.state; + TupleDesc relDesc = RelationGetDescr(resultRelInfo->ri_RelationDesc); + int whichrel; + List *updateColnos; + + /* + * Usually, mt_lastResultIndex matches the target rel. If it happens not + * to, we can get the index the hard way with an integer division. + */ + whichrel = mtstate->mt_lastResultIndex; + if (resultRelInfo != mtstate->resultRelInfo + whichrel) + { + whichrel = resultRelInfo - mtstate->resultRelInfo; + Assert(whichrel >= 0 && whichrel < mtstate->mt_nrels); + } + + updateColnos = (List *) list_nth(node->updateColnosLists, whichrel); + + /* + * For UPDATE, we use the old tuple to fill up missing values in the tuple + * produced by the subplan to get the new tuple. We need two slots, both + * matching the table's desired format. + */ + resultRelInfo->ri_oldTupleSlot = + table_slot_create(resultRelInfo->ri_RelationDesc, + &estate->es_tupleTable); + resultRelInfo->ri_newTupleSlot = + table_slot_create(resultRelInfo->ri_RelationDesc, + &estate->es_tupleTable); + + /* need an expression context to do the projection */ + if (mtstate->ps.ps_ExprContext == NULL) + ExecAssignExprContext(estate, &mtstate->ps); + + resultRelInfo->ri_projectNew = + ExecBuildUpdateProjection(subplan->targetlist, + updateColnos, + relDesc, + mtstate->ps.ps_ExprContext, + resultRelInfo->ri_newTupleSlot, + &mtstate->ps); + + resultRelInfo->ri_projectNewInfoValid = true; +} + /* * ExecGetInsertNewTuple * This prepares a "new" tuple ready to be inserted into given result @@ -429,6 +562,8 @@ ExecGetUpdateNewTuple(ResultRelInfo *relinfo, ProjectionInfo *newProj = relinfo->ri_projectNew; ExprContext *econtext; + /* Use a few extra Asserts to protect against outside callers */ + Assert(relinfo->ri_projectNewInfoValid); Assert(planSlot != NULL && !TTS_EMPTY(planSlot)); Assert(oldSlot != NULL && !TTS_EMPTY(oldSlot)); @@ -1375,8 +1510,12 @@ ExecCrossPartitionUpdate(ModifyTableState *mtstate, else { /* Fetch the most recent version of old tuple. */ - TupleTableSlot *oldSlot = resultRelInfo->ri_oldTupleSlot; + TupleTableSlot *oldSlot; + /* ... but first, make sure ri_oldTupleSlot is initialized. */ + if (unlikely(!resultRelInfo->ri_projectNewInfoValid)) + ExecInitUpdateProjection(mtstate, resultRelInfo); + oldSlot = resultRelInfo->ri_oldTupleSlot; if (!table_tuple_fetch_row_version(resultRelInfo->ri_RelationDesc, tupleid, SnapshotAny, @@ -1706,6 +1845,10 @@ lreplace:; /* Tuple not passing quals anymore, exiting... */ return NULL; + /* Make sure ri_oldTupleSlot is initialized. */ + if (unlikely(!resultRelInfo->ri_projectNewInfoValid)) + ExecInitUpdateProjection(mtstate, resultRelInfo); + /* Fetch the most recent version of old tuple. */ oldSlot = resultRelInfo->ri_oldTupleSlot; if (!table_tuple_fetch_row_version(resultRelationDesc, @@ -2388,11 +2531,17 @@ ExecModifyTable(PlanState *pstate) switch (operation) { case CMD_INSERT: + /* Initialize projection info if first time for this table */ + if (unlikely(!resultRelInfo->ri_projectNewInfoValid)) + ExecInitInsertProjection(node, resultRelInfo); slot = ExecGetInsertNewTuple(resultRelInfo, planSlot); slot = ExecInsert(node, resultRelInfo, slot, planSlot, estate, node->canSetTag); break; case CMD_UPDATE: + /* Initialize projection info if first time for this table */ + if (unlikely(!resultRelInfo->ri_projectNewInfoValid)) + ExecInitUpdateProjection(node, resultRelInfo); /* * Make the new tuple by combining plan's output tuple with @@ -2665,8 +2814,65 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) i, eflags); } + + /* + * For UPDATE/DELETE, find the appropriate junk attr now, either a + * 'ctid' or 'wholerow' attribute depending on relkind. For foreign + * tables, the FDW might have created additional junk attr(s), but + * those are no concern of ours. + */ + if (operation == CMD_UPDATE || operation == CMD_DELETE) + { + char relkind; + + relkind = resultRelInfo->ri_RelationDesc->rd_rel->relkind; + if (relkind == RELKIND_RELATION || + relkind == RELKIND_MATVIEW || + relkind == RELKIND_PARTITIONED_TABLE) + { + resultRelInfo->ri_RowIdAttNo = + ExecFindJunkAttributeInTlist(subplan->targetlist, "ctid"); + if (!AttributeNumberIsValid(resultRelInfo->ri_RowIdAttNo)) + elog(ERROR, "could not find junk ctid column"); + } + else if (relkind == RELKIND_FOREIGN_TABLE) + { + /* + * When there is a row-level trigger, there should be a + * wholerow attribute. We also require it to be present in + * UPDATE, so we can get the values of unchanged columns. + */ + resultRelInfo->ri_RowIdAttNo = + ExecFindJunkAttributeInTlist(subplan->targetlist, + "wholerow"); + if (mtstate->operation == CMD_UPDATE && + !AttributeNumberIsValid(resultRelInfo->ri_RowIdAttNo)) + elog(ERROR, "could not find junk wholerow column"); + } + else + { + /* Other valid target relkinds must provide wholerow */ + resultRelInfo->ri_RowIdAttNo = + ExecFindJunkAttributeInTlist(subplan->targetlist, + "wholerow"); + if (!AttributeNumberIsValid(resultRelInfo->ri_RowIdAttNo)) + elog(ERROR, "could not find junk wholerow column"); + } + } } + /* + * If this is an inherited update/delete, there will be a junk attribute + * named "tableoid" present in the subplan's targetlist. It will be used + * to identify the result relation for a given tuple to be + * updated/deleted. + */ + mtstate->mt_resultOidAttno = + ExecFindJunkAttributeInTlist(subplan->targetlist, "tableoid"); + Assert(AttributeNumberIsValid(mtstate->mt_resultOidAttno) || nrels == 1); + mtstate->mt_lastResultOid = InvalidOid; /* force lookup at first tuple */ + mtstate->mt_lastResultIndex = 0; /* must be zero if no such attr */ + /* Get the root target relation */ rel = mtstate->rootResultRelInfo->ri_RelationDesc; @@ -2842,163 +3048,6 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags) EvalPlanQualSetPlan(&mtstate->mt_epqstate, subplan, arowmarks); - /* - * Initialize projection(s) to create tuples suitable for result rel(s). - * INSERT queries may need a projection to filter out junk attrs in the - * tlist. UPDATE always needs a projection, because (1) there's always - * some junk attrs, and (2) we may need to merge values of not-updated - * columns from the old tuple into the final tuple. In UPDATE, the tuple - * arriving from the subplan contains only new values for the changed - * columns, plus row identity info in the junk attrs. - * - * If there are multiple result relations, each one needs its own - * projection. Note multiple rels are only possible for UPDATE/DELETE, so - * we can't be fooled by some needing a projection and some not. - * - * This section of code is also a convenient place to verify that the - * output of an INSERT or UPDATE matches the target table(s). - */ - for (i = 0; i < nrels; i++) - { - resultRelInfo = &mtstate->resultRelInfo[i]; - - /* - * Prepare to generate tuples suitable for the target relation. - */ - if (operation == CMD_INSERT) - { - List *insertTargetList = NIL; - bool need_projection = false; - - foreach(l, subplan->targetlist) - { - TargetEntry *tle = (TargetEntry *) lfirst(l); - - if (!tle->resjunk) - insertTargetList = lappend(insertTargetList, tle); - else - need_projection = true; - } - - /* - * The junk-free list must produce a tuple suitable for the result - * relation. - */ - ExecCheckPlanOutput(resultRelInfo->ri_RelationDesc, - insertTargetList); - - /* We'll need a slot matching the table's format. */ - resultRelInfo->ri_newTupleSlot = - table_slot_create(resultRelInfo->ri_RelationDesc, - &mtstate->ps.state->es_tupleTable); - - /* Build ProjectionInfo if needed (it probably isn't). */ - if (need_projection) - { - TupleDesc relDesc = RelationGetDescr(resultRelInfo->ri_RelationDesc); - - /* need an expression context to do the projection */ - if (mtstate->ps.ps_ExprContext == NULL) - ExecAssignExprContext(estate, &mtstate->ps); - - resultRelInfo->ri_projectNew = - ExecBuildProjectionInfo(insertTargetList, - mtstate->ps.ps_ExprContext, - resultRelInfo->ri_newTupleSlot, - &mtstate->ps, - relDesc); - } - } - else if (operation == CMD_UPDATE) - { - List *updateColnos; - TupleDesc relDesc = RelationGetDescr(resultRelInfo->ri_RelationDesc); - - updateColnos = (List *) list_nth(node->updateColnosLists, i); - - /* - * For UPDATE, we use the old tuple to fill up missing values in - * the tuple produced by the plan to get the new tuple. We need - * two slots, both matching the table's desired format. - */ - resultRelInfo->ri_oldTupleSlot = - table_slot_create(resultRelInfo->ri_RelationDesc, - &mtstate->ps.state->es_tupleTable); - resultRelInfo->ri_newTupleSlot = - table_slot_create(resultRelInfo->ri_RelationDesc, - &mtstate->ps.state->es_tupleTable); - - /* need an expression context to do the projection */ - if (mtstate->ps.ps_ExprContext == NULL) - ExecAssignExprContext(estate, &mtstate->ps); - - resultRelInfo->ri_projectNew = - ExecBuildUpdateProjection(subplan->targetlist, - updateColnos, - relDesc, - mtstate->ps.ps_ExprContext, - resultRelInfo->ri_newTupleSlot, - &mtstate->ps); - } - - /* - * For UPDATE/DELETE, find the appropriate junk attr now, either a - * 'ctid' or 'wholerow' attribute depending on relkind. For foreign - * tables, the FDW might have created additional junk attr(s), but - * those are no concern of ours. - */ - if (operation == CMD_UPDATE || operation == CMD_DELETE) - { - char relkind; - - relkind = resultRelInfo->ri_RelationDesc->rd_rel->relkind; - if (relkind == RELKIND_RELATION || - relkind == RELKIND_MATVIEW || - relkind == RELKIND_PARTITIONED_TABLE) - { - resultRelInfo->ri_RowIdAttNo = - ExecFindJunkAttributeInTlist(subplan->targetlist, "ctid"); - if (!AttributeNumberIsValid(resultRelInfo->ri_RowIdAttNo)) - elog(ERROR, "could not find junk ctid column"); - } - else if (relkind == RELKIND_FOREIGN_TABLE) - { - /* - * When there is a row-level trigger, there should be a - * wholerow attribute. We also require it to be present in - * UPDATE, so we can get the values of unchanged columns. - */ - resultRelInfo->ri_RowIdAttNo = - ExecFindJunkAttributeInTlist(subplan->targetlist, - "wholerow"); - if (mtstate->operation == CMD_UPDATE && - !AttributeNumberIsValid(resultRelInfo->ri_RowIdAttNo)) - elog(ERROR, "could not find junk wholerow column"); - } - else - { - /* Other valid target relkinds must provide wholerow */ - resultRelInfo->ri_RowIdAttNo = - ExecFindJunkAttributeInTlist(subplan->targetlist, - "wholerow"); - if (!AttributeNumberIsValid(resultRelInfo->ri_RowIdAttNo)) - elog(ERROR, "could not find junk wholerow column"); - } - } - } - - /* - * If this is an inherited update/delete, there will be a junk attribute - * named "tableoid" present in the subplan's targetlist. It will be used - * to identify the result relation for a given tuple to be - * updated/deleted. - */ - mtstate->mt_resultOidAttno = - ExecFindJunkAttributeInTlist(subplan->targetlist, "tableoid"); - Assert(AttributeNumberIsValid(mtstate->mt_resultOidAttno) || nrels == 1); - mtstate->mt_lastResultOid = InvalidOid; /* force lookup at first tuple */ - mtstate->mt_lastResultIndex = 0; /* must be zero if no such attr */ - /* * If there are a lot of result relations, use a hash table to speed the * lookups. If there are not a lot, a simple linear search is faster. diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 8116d62e814..e7ae21c023c 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -431,6 +431,8 @@ typedef struct ResultRelInfo TupleTableSlot *ri_newTupleSlot; /* Slot to hold the old tuple being updated */ TupleTableSlot *ri_oldTupleSlot; + /* Have the projection and the slots above been initialized? */ + bool ri_projectNewInfoValid; /* triggers to be fired, if any */ TriggerDesc *ri_TrigDesc;