From 9c52d8990f087189137e945bdc6dfde062a295ac Mon Sep 17 00:00:00 2001 From: Robert Haas Date: Mon, 20 Oct 2025 12:00:18 -0400 Subject: [PATCH] Store information about range-table flattening in the final plan. Suppose that we're currently planning a query and, when that same query was previously planned and executed, we learned something about how a certain table within that query should be planned. We want to take note when that same table is being planned during the current planning cycle, but this is difficult to do, because the RTI of the table from the previous plan won't necessarily be equal to the RTI that we see during the current planning cycle. This is because each subquery has a separate range table during planning, but these are flattened into one range table when constructing the final plan, changing RTIs. Commit 8c49a484e8ebb0199fba4bd68eaaedaf49b48ed0 allows us to match up subqueries seen in the previous planning cycles with the subqueries currently being planned just by comparing textual names, but that's not quite enough to let us deduce anything about individual tables, because we don't know where each subquery's range table appears in the final, flattened range table. To fix that, store a list of SubPlanRTInfo objects in the final planned statement, each including the name of the subplan, the offset at which it begins in the flattened range table, and whether or not it was a dummy subplan -- if it was, some RTIs may have been dropped from the final range table, but also there's no need to control how a dummy subquery gets planned. The toplevel subquery has no name and always begins at rtoffset 0, so we make no entry for it. This commit teaches pg_overexplain's RANGE_TABLE option to make use of this new data to display the subquery name for each range table entry. NOTE TO REVIEWERS: If there's a clean way to make pg_overexplain display this information without the new infrastructure provided by this patch, then this patch is unnecessary. I thought there would be a way to do that, but I couldn't figure anything out: there seems to be nothing that records in the final PlannedStmt where subquery's range table ends and the next one begins. In practice, one could usually figure it out by matching up tables by relation OID, but that's neither clean nor theoretically sound. --- contrib/pg_overexplain/pg_overexplain.c | 36 +++++++++++++++++++++++++ src/backend/optimizer/plan/planner.c | 1 + src/backend/optimizer/plan/setrefs.c | 20 ++++++++++++++ src/include/nodes/pathnodes.h | 3 +++ src/include/nodes/plannodes.h | 17 ++++++++++++ src/tools/pgindent/typedefs.list | 1 + 6 files changed, 78 insertions(+) diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c index fcdc17012d..1c4c796adb 100644 --- a/contrib/pg_overexplain/pg_overexplain.c +++ b/contrib/pg_overexplain/pg_overexplain.c @@ -395,6 +395,8 @@ static void overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es) { Index rti; + ListCell *lc_subrtinfo = list_head(plannedstmt->subrtinfos); + SubPlanRTInfo *rtinfo = NULL; /* Open group, one entry per RangeTblEntry */ ExplainOpenGroup("Range Table", "Range Table", false, es); @@ -405,6 +407,18 @@ overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es) RangeTblEntry *rte = rt_fetch(rti, plannedstmt->rtable); char *kind = NULL; char *relkind; + SubPlanRTInfo *next_rtinfo; + + /* Advance to next SubRTInfo, if it's time. */ + if (lc_subrtinfo != NULL) + { + next_rtinfo = lfirst(lc_subrtinfo); + if (rti > next_rtinfo->rtoffset) + { + rtinfo = next_rtinfo; + lc_subrtinfo = lnext(plannedstmt->subrtinfos, lc_subrtinfo); + } + } /* NULL entries are possible; skip them */ if (rte == NULL) @@ -469,6 +483,28 @@ overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es) ExplainPropertyBool("In From Clause", rte->inFromCl, es); } + /* + * Indicate which subplan is the origin of which RTE. Note dummy + * subplans. Here again, we crunch more onto one line in text format. + */ + if (rtinfo != NULL) + { + if (es->format == EXPLAIN_FORMAT_TEXT) + { + if (!rtinfo->dummy) + ExplainPropertyText("Subplan", rtinfo->plan_name, es); + else + ExplainPropertyText("Subplan", + psprintf("%s (dummy)", + rtinfo->plan_name), es); + } + else + { + ExplainPropertyText("Subplan", rtinfo->plan_name, es); + ExplainPropertyBool("Subplan Is Dummy", rtinfo->dummy, es); + } + } + /* rte->alias is optional; rte->eref is requested */ if (rte->alias != NULL) overexplain_alias("Alias", rte->alias, es); diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 8b22c30559..31dcbdf342 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -607,6 +607,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions, result->unprunableRelids = bms_difference(glob->allRelids, glob->prunableRelids); result->permInfos = glob->finalrteperminfos; + result->subrtinfos = glob->subrtinfos; result->resultRelations = glob->resultRelations; result->appendRelations = glob->appendRelations; result->subplans = glob->subplans; diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index cd7ea1e6b5..edcd4aaa53 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -399,6 +399,26 @@ add_rtes_to_flat_rtable(PlannerInfo *root, bool recursing) Index rti; ListCell *lc; + /* + * Record enough information to make it possible for code that looks at + * the final range table to understand how it was constructed. (If + * finalrtable is still NIL, then this is the very topmost PlannerInfo, + * which will always have plan_name == NULL and rtoffset == 0; we omit the + * degenerate list entry.) + */ + if (root->glob->finalrtable != NIL) + { + SubPlanRTInfo *rtinfo = makeNode(SubPlanRTInfo); + + rtinfo->plan_name = root->plan_name; + rtinfo->rtoffset = list_length(root->glob->finalrtable); + + /* When recursing = true, it's an unplanned or dummy subquery. */ + rtinfo->dummy = recursing; + + root->glob->subrtinfos = lappend(root->glob->subrtinfos, rtinfo); + } + /* * Add the query's own RTEs to the flattened rangetable. * diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index 46a8655621..3782bc6407 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -135,6 +135,9 @@ typedef struct PlannerGlobal /* "flat" list of RTEPermissionInfos */ List *finalrteperminfos; + /* list of SubPlanRTInfo nodes */ + List *subrtinfos; + /* "flat" list of PlanRowMarks */ List *finalrowmarks; diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index c4393a9432..1526dd2ec6 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -131,6 +131,9 @@ typedef struct PlannedStmt */ List *subplans; + /* a list of SubPlanRTInfo objects */ + List *subrtinfos; + /* indices of subplans that require REWIND */ Bitmapset *rewindPlanIDs; @@ -1821,4 +1824,18 @@ typedef enum MonotonicFunction MONOTONICFUNC_BOTH = MONOTONICFUNC_INCREASING | MONOTONICFUNC_DECREASING, } MonotonicFunction; +/* + * SubPlanRTInfo + * + * Information about which range table entries came from which subquery + * planning cycles. + */ +typedef struct SubPlanRTInfo +{ + NodeTag type; + const char *plan_name; + Index rtoffset; + bool dummy; +} SubPlanRTInfo; + #endif /* PLANNODES_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9dd65b1025..025bba8783 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2902,6 +2902,7 @@ SubLink SubLinkType SubOpts SubPlan +SubPlanRTInfo SubPlanState SubRelInfo SubRemoveRels -- 2.39.5