v9-0003-Store-information-about-elided-nodes-in-the-final.patch

application/octet-stream

Filename: v9-0003-Store-information-about-elided-nodes-in-the-final.patch
Type: application/octet-stream
Part: 3
Message: Re: plan shape work

Patch

Same data as JSON: GET /api/v1/attachments/:id/patch the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes. API reference →
Format: format-patch
Series: patch v9-0003
Subject: Store information about elided nodes in the final plan.
File+
contrib/pg_overexplain/expected/pg_overexplain.out 3 1
contrib/pg_overexplain/pg_overexplain.c 39 0
src/backend/optimizer/plan/planner.c 1 0
src/backend/optimizer/plan/setrefs.c 50 2
src/include/nodes/pathnodes.h 3 0
src/include/nodes/plannodes.h 17 0
src/tools/pgindent/typedefs.list 1 0
From e8e329c64a2eda3a664d6e7e7ffd94ce3c3a2e59 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Tue, 22 Apr 2025 14:10:19 -0400
Subject: [PATCH v9 3/4] Store information about elided nodes in the final
 plan.

When setrefs.c removes a SubqueryScan, single-child Append, or
single-child MergeAppend from the final Plan tree, the RTI which
would have been scanned by the removed node no longer appears in
the final plan (the actual range table entry is still present,
but it's no longer referenced).

That's fine for the executor, but it can create difficulties for
code that wants to deduce from the final plan what choices were
made during the planing process. For example, a traversal of a
join tree in the final plan might never encounter the RTI of one
of the relationss in the join problem, and might instead encounter
a scan of a child RTI or even one from a different subquery level.

This patch adjusts things so that each time we elide a node during
setrefs processing, we record the plan_node_id of its single surviving
child, the type of the removed node, and the RTIs that the removed
node would have scanned. This information is recorded in a separate
list that can be ignored by the executor and examined only by code
that cares about these details.

This commit also updates pg_overexplain to display these details.
---
 .../expected/pg_overexplain.out               |  4 +-
 contrib/pg_overexplain/pg_overexplain.c       | 39 ++++++++++++++
 src/backend/optimizer/plan/planner.c          |  1 +
 src/backend/optimizer/plan/setrefs.c          | 52 ++++++++++++++++++-
 src/include/nodes/pathnodes.h                 |  3 ++
 src/include/nodes/plannodes.h                 | 17 ++++++
 src/tools/pgindent/typedefs.list              |  1 +
 7 files changed, 114 insertions(+), 3 deletions(-)

diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out
index 55d34666d87..ca9a23ea61f 100644
--- a/contrib/pg_overexplain/expected/pg_overexplain.out
+++ b/contrib/pg_overexplain/expected/pg_overexplain.out
@@ -452,6 +452,8 @@ SELECT * FROM vegetables WHERE genus = 'daucus';
  Seq Scan on daucus vegetables
    Filter: (genus = 'daucus'::text)
    Scan RTI: 2
+   Elided Node Type: Append
+   Elided Node RTIs: 1
  RTI 1 (relation, inherited, in-from-clause):
    Eref: vegetables (id, name, genus)
    Relation: vegetables
@@ -465,7 +467,7 @@ SELECT * FROM vegetables WHERE genus = 'daucus';
    Relation Kind: relation
    Relation Lock Mode: AccessShareLock
  Unprunable RTIs: 1 2
-(16 rows)
+(18 rows)
 
 -- Also test a case that involves a write.
 EXPLAIN (RANGE_TABLE, COSTS OFF)
diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index 5dc707d69e3..fa907fa472e 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -191,6 +191,8 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors,
 	 */
 	if (options->range_table)
 	{
+		bool		opened_elided_nodes = false;
+
 		switch (nodeTag(plan))
 		{
 			case T_SeqScan:
@@ -251,6 +253,43 @@ overexplain_per_node_hook(PlanState *planstate, List *ancestors,
 			default:
 				break;
 		}
+
+		foreach_node(ElidedNode, n, es->pstmt->elidedNodes)
+		{
+			char	   *elidednodetag;
+
+			if (n->plan_node_id != plan->plan_node_id)
+				continue;
+
+			if (!opened_elided_nodes)
+			{
+				ExplainOpenGroup("Elided Nodes", "Elided Nodes", false, es);
+				opened_elided_nodes = true;
+			}
+
+			switch (n->elided_type)
+			{
+				case T_Append:
+					elidednodetag = "Append";
+					break;
+				case T_MergeAppend:
+					elidednodetag = "MergeAppend";
+					break;
+				case T_SubqueryScan:
+					elidednodetag = "SubqueryScan";
+					break;
+				default:
+					elidednodetag = psprintf("%d", n->elided_type);
+					break;
+			}
+
+			ExplainOpenGroup("Elided Node", NULL, true, es);
+			ExplainPropertyText("Elided Node Type", elidednodetag, es);
+			overexplain_bitmapset("Elided Node RTIs", n->relids, es);
+			ExplainCloseGroup("Elided Node", NULL, true, es);
+		}
+		if (opened_elided_nodes)
+			ExplainCloseGroup("Elided Nodes", "Elided Nodes", false, es);
 	}
 }
 
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index d47c2915b3a..d977cbc92ad 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -590,6 +590,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	result->paramExecTypes = glob->paramExecTypes;
 	/* utilityStmt should be null, but we might as well copy it */
 	result->utilityStmt = parse->utilityStmt;
+	result->elidedNodes = glob->elidedNodes;
 	result->stmt_location = parse->stmt_location;
 	result->stmt_len = parse->stmt_len;
 
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index eef43792aeb..5900458a0e1 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -211,6 +211,9 @@ static List *set_windowagg_runcondition_references(PlannerInfo *root,
 												   List *runcondition,
 												   Plan *plan);
 
+static void record_elided_node(PlannerGlobal *glob, int plan_node_id,
+							   NodeTag elided_type, Bitmapset *relids);
+
 
 /*****************************************************************************
  *
@@ -1441,10 +1444,17 @@ set_subqueryscan_references(PlannerInfo *root,
 
 	if (trivial_subqueryscan(plan))
 	{
+		Index		scanrelid;
+
 		/*
 		 * We can omit the SubqueryScan node and just pull up the subplan.
 		 */
 		result = clean_up_removed_plan_level((Plan *) plan, plan->subplan);
+
+		/* Remember that we removed a SubqueryScan */
+		scanrelid = plan->scan.scanrelid + rtoffset;
+		record_elided_node(root->glob, plan->subplan->plan_node_id,
+						   T_SubqueryScan, bms_make_singleton(scanrelid));
 	}
 	else
 	{
@@ -1872,7 +1882,17 @@ set_append_references(PlannerInfo *root,
 		Plan	   *p = (Plan *) linitial(aplan->appendplans);
 
 		if (p->parallel_aware == aplan->plan.parallel_aware)
-			return clean_up_removed_plan_level((Plan *) aplan, p);
+		{
+			Plan	   *result;
+
+			result = clean_up_removed_plan_level((Plan *) aplan, p);
+
+			/* Remember that we removed an Append */
+			record_elided_node(root->glob, p->plan_node_id, T_Append,
+							   offset_relid_set(aplan->apprelids, rtoffset));
+
+			return result;
+		}
 	}
 
 	/*
@@ -1940,7 +1960,17 @@ set_mergeappend_references(PlannerInfo *root,
 		Plan	   *p = (Plan *) linitial(mplan->mergeplans);
 
 		if (p->parallel_aware == mplan->plan.parallel_aware)
-			return clean_up_removed_plan_level((Plan *) mplan, p);
+		{
+			Plan	   *result;
+
+			result = clean_up_removed_plan_level((Plan *) mplan, p);
+
+			/* Remember that we removed a MergeAppend */
+			record_elided_node(root->glob, p->plan_node_id, T_MergeAppend,
+							   offset_relid_set(mplan->apprelids, rtoffset));
+
+			return result;
+		}
 	}
 
 	/*
@@ -3755,3 +3785,21 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context)
 	return expression_tree_walker(node, extract_query_dependencies_walker,
 								  context);
 }
+
+/*
+ * Record some details about a node removed from the plan during setrefs
+ * procesing, for the benefit of code trying to reconstruct planner decisions
+ * from examination of the final plan tree.
+ */
+static void
+record_elided_node(PlannerGlobal *glob, int plan_node_id,
+				   NodeTag elided_type, Bitmapset *relids)
+{
+	ElidedNode *n = makeNode(ElidedNode);
+
+	n->plan_node_id = plan_node_id;
+	n->elided_type = elided_type;
+	n->relids = relids;
+
+	glob->elidedNodes = lappend(glob->elidedNodes, n);
+}
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index e14cbd383fc..3bff0763f7b 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -159,6 +159,9 @@ typedef struct PlannerGlobal
 	/* type OIDs for PARAM_EXEC Params */
 	List	   *paramExecTypes;
 
+	/* info about nodes elided from the plan during setrefs processing */
+	List	   *elidedNodes;
+
 	/* highest PlaceHolderVar ID assigned */
 	Index		lastPHId;
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index a54f42524b0..218f8f1ad6d 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -152,6 +152,9 @@ typedef struct PlannedStmt
 	/* non-null if this is utility stmt */
 	Node	   *utilityStmt;
 
+	/* info about nodes elided from the plan during setrefs processing */
+	List	   *elidedNodes;
+
 	/* statement location in source string (copied from Query) */
 	/* start location, or -1 if unknown */
 	ParseLoc	stmt_location;
@@ -1831,4 +1834,18 @@ typedef struct SubPlanRTInfo
 	bool		dummy;
 } SubPlanRTInfo;
 
+/*
+ * ElidedNode
+ *
+ * Information about nodes elided from the final plan tree: trivial subquery
+ * scans, and single-child Append and MergeAppend nodes.
+ */
+typedef struct ElidedNode
+{
+	NodeTag		type;
+	int			plan_node_id;
+	NodeTag		elided_type;
+	Bitmapset  *relids;
+} ElidedNode;
+
 #endif							/* PLANNODES_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 993688870db..b9f71bc5271 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -4344,3 +4344,4 @@ z_stream
 z_streamp
 zic_t
 SubPlanRTInfo
+ElidedNode
-- 
2.39.5 (Apple Git-154)