From 49518713775c3d24ba8e7a802c817042143e548a Mon Sep 17 00:00:00 2001
From: Andrey Kazachkov <andrey.kazachkov@tantorlabs.ru>
Date: Tue, 16 Dec 2025 19:52:39 +0300
Subject: [PATCH v6 1/2] Added plan_id to the core.

1. Changed a jumbling initialization routine
2. JumbleNode() is an entry point for jumbling
3. AppendJumble() remains static
4. guc-compute-plan-id has been added
5. gen_node_support.pl changed to process plan node structures
6. pg_node_attr has been added to certain structures/fields
---
 doc/src/sgml/config.sgml                      |  34 ++++
 doc/src/sgml/monitoring.sgml                  |  16 ++
 src/backend/catalog/system_views.sql          |   1 +
 src/backend/commands/explain.c                |  17 ++
 src/backend/executor/execMain.c               |  10 +-
 src/backend/nodes/gen_node_support.pl         |  34 +++-
 src/backend/nodes/queryjumblefuncs.c          | 180 +++++++++++-------
 src/backend/optimizer/plan/planner.c          |  19 ++
 src/backend/optimizer/plan/setrefs.c          |   9 +
 src/backend/postmaster/launch_backend.c       |   3 +
 src/backend/utils/adt/pgstatfuncs.c           |   7 +-
 src/backend/utils/misc/guc_parameters.dat     |   7 +
 src/backend/utils/misc/guc_tables.c           |  18 ++
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/catalog/pg_proc.dat               |   6 +-
 src/include/nodes/pathnodes.h                 |   3 +
 src/include/nodes/plannodes.h                 |  50 ++---
 src/include/nodes/primnodes.h                 |   4 +-
 src/include/nodes/queryjumble.h               |  35 +++-
 src/include/utils/backend_status.h            |   2 +-
 src/test/regress/expected/explain.out         |  11 ++
 src/test/regress/expected/rules.out           |   9 +-
 src/test/regress/sql/explain.sql              |   4 +
 23 files changed, 371 insertions(+), 109 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 405c9689bd0..4cdaf9c49ba 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8864,6 +8864,40 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-compute-plan-id" xreflabel="compute_plan_id">
+      <term><varname>compute_plan_id</varname> (<type>enum</type>)
+      <indexterm>
+       <primary><varname>compute_plan_id</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Enables in-core computation of a plan identifier.
+        Plan identifiers can be displayed in the <link
+        linkend="monitoring-pg-stat-activity-view"><structname>pg_stat_activity</structname></link>
+        view or using <command>EXPLAIN</command>.
+        Note that an external module can alternatively be used if the
+        in-core plan identifier computation method is not acceptable.
+        In this case, in-core computation must be always disabled.
+        Valid values are <literal>off</literal> (always disabled),
+        <literal>on</literal> (always enabled), <literal>auto</literal>,
+        which lets modules that utilize plan identifiers enable
+        it automatically, and <literal>regress</literal> which
+        has the same effect as <literal>on</literal>, except that the
+        query identifier is not shown in the <literal>EXPLAIN</literal> output
+        in order to facilitate automated regression testing.
+        The default is <literal>auto</literal>.
+       </para>
+       <note>
+        <para>
+         To ensure that only one plan identifier is calculated and
+         displayed, extensions that calculate plan identifiers should
+         throw an error if a plan identifier has already been computed.
+        </para>
+       </note>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-log-statement-stats">
       <term><varname>log_statement_stats</varname> (<type>boolean</type>)
       <indexterm>
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 817fd9f4ca7..9e98fd2c198 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -983,6 +983,22 @@ postgres   27093  0.0  0.0  30096  2752 ?        Ss   11:34   0:00 postgres: ser
      </para></entry>
     </row>
 
+     <row>
+       <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>plan_id</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Identifier of this backend's most recent query plan. If
+       <structfield>state</structfield> is <literal>active</literal> this
+       field shows the identifier of the currently executing query plan. In
+       all other states, it shows the identifier of last query plan that
+       was executed.  Plan identifiers are not computed by default so this
+       field will be null unless <xref linkend="guc-compute-plan-id"/>
+       parameter is enabled or a third-party module that computes plan
+       identifiers is configured.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>query</structfield> <type>text</type>
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 0a0f95f6bb9..35744a82f0b 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -919,6 +919,7 @@ CREATE VIEW pg_stat_activity AS
             S.backend_xid,
             S.backend_xmin,
             S.query_id,
+			S.plan_id,
             S.query,
             S.backend_type
     FROM pg_stat_get_activity(NULL) AS S
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 5a6390631eb..b21cd142111 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -818,6 +818,23 @@ ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc)
 		ExplainPropertyInteger("Query Identifier", NULL,
 							   queryDesc->plannedstmt->queryId, es);
 	}
+
+	/*
+	 * COMPUTE_PLAN_ID_REGRESS means COMPUTE_PLAN_ID_YES, but we don't show
+	 * the queryid in any of the EXPLAIN plans to keep stable the results
+	 * generated by regression test suites.
+	 */
+	if (es->verbose && queryDesc->plannedstmt->planId != UINT64CONST(0) &&
+		compute_plan_id != COMPUTE_PLAN_ID_REGRESS)
+	{
+		/*
+		 * Output the queryid as an int64 rather than a uint64 so we match
+		 * what would be seen in the BIGINT pg_stat_activity.plan_id column.
+		 */
+		ExplainPropertyInteger("Plan Identifier", NULL,
+							   queryDesc->plannedstmt->planId, es);
+	}
+
 }
 
 /*
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 797d8b1ca1c..14f107d45b1 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -123,13 +123,15 @@ ExecutorStart(QueryDesc *queryDesc, int eflags)
 {
 	/*
 	 * In some cases (e.g. an EXECUTE statement or an execute message with the
-	 * extended query protocol) the query_id won't be reported, so do it now.
+	 * extended query protocol) the query_id and plan_id won't be reported, so
+	 * do it now.
 	 *
-	 * Note that it's harmless to report the query_id multiple times, as the
-	 * call will be ignored if the top level query_id has already been
-	 * reported.
+	 * Note that it's harmless to report the identifiers multiple times, as
+	 * the call will be ignored if the top level query_id or plan_id has
+	 * already been reported.
 	 */
 	pgstat_report_query_id(queryDesc->plannedstmt->queryId, false);
+	pgstat_report_plan_id(queryDesc->plannedstmt->planId, false);
 
 	if (ExecutorStart_hook)
 		(*ExecutorStart_hook) (queryDesc, eflags);
diff --git a/src/backend/nodes/gen_node_support.pl b/src/backend/nodes/gen_node_support.pl
index 9ecddb14231..eab144bd962 100644
--- a/src/backend/nodes/gen_node_support.pl
+++ b/src/backend/nodes/gen_node_support.pl
@@ -1291,6 +1291,7 @@ _jumble${n}(JumbleState *jstate, Node *node)
 	{
 		my $t = $node_type_info{$n}->{field_types}{$f};
 		my @a = @{ $node_type_info{$n}->{field_attrs}{$f} };
+		my $array_size_field;
 		my $query_jumble_ignore = $struct_no_query_jumble;
 		my $query_jumble_custom = 0;
 		my $query_jumble_location = 0;
@@ -1303,7 +1304,11 @@ _jumble${n}(JumbleState *jstate, Node *node)
 			{
 				$query_jumble_custom = 1;
 			}
-			if ($a eq 'query_jumble_ignore')
+			elsif ($a =~ /^array_size\(([\w.]+)\)$/)
+			{
+				$array_size_field = $1;
+			}
+			elsif ($a eq 'query_jumble_ignore')
 			{
 				$query_jumble_ignore = 1;
 			}
@@ -1317,12 +1322,19 @@ _jumble${n}(JumbleState *jstate, Node *node)
 			}
 		}
 
+		next if $query_jumble_ignore;
+
 		if ($query_jumble_custom)
 		{
 			# Custom function that applies to one field of a node.
 			print $jff "\tJUMBLE_CUSTOM($n, $f);\n"
 			  unless $query_jumble_ignore;
 		}
+		elsif ($t eq 'Bitmapset*')
+		{
+			print $jff "\tJUMBLE_BITMAPSET($f);\n"
+			  unless $query_jumble_ignore;
+		}
 		elsif (($t =~ /^(\w+)\*$/ or $t =~ /^struct\s+(\w+)\*$/)
 			and elem $1, @node_types)
 		{
@@ -1352,6 +1364,26 @@ _jumble${n}(JumbleState *jstate, Node *node)
 			print $jff "\tJUMBLE_STRING($f);\n"
 			  unless $query_jumble_ignore;
 		}
+		elsif ($t =~ /^(\w+)(\*|\[\w+\])$/ and elem $1, @scalar_types)
+		{
+			if (!defined $array_size_field)
+			{
+				die "no array size defined for $n.$f of type $t\n";
+			}
+			if ($node_type_info{$n}->{field_types}{$array_size_field} eq
+				'List*')
+			{
+				print $jff
+				  "\tJUMBLE_ARRAY($f, list_length(expr->$array_size_field));\n"
+				  unless $query_jumble_ignore;
+			}
+			else
+			{
+				print $jff
+				  "\tJUMBLE_ARRAY($f, expr->$array_size_field);\n"
+				  unless $query_jumble_ignore;
+			}
+		}
 		else
 		{
 			print $jff "\tJUMBLE_FIELD($f);\n"
diff --git a/src/backend/nodes/queryjumblefuncs.c b/src/backend/nodes/queryjumblefuncs.c
index ffc230af427..5717480dbf5 100644
--- a/src/backend/nodes/queryjumblefuncs.c
+++ b/src/backend/nodes/queryjumblefuncs.c
@@ -3,32 +3,31 @@
  * queryjumblefuncs.c
  *	 Query normalization and fingerprinting.
  *
- * Normalization is a process whereby similar queries, typically differing only
- * in their constants (though the exact rules are somewhat more subtle than
- * that) are recognized as equivalent, and are tracked as a single entry.  This
- * is particularly useful for non-prepared queries.
+ * Fingerprinting selectively serializes key fields within a tree structure,
+ * such as a Query or Plan tree, to create a unique identifier while ignoring
+ * extraneous details. These essential fields are concatenated into a jumble,
+ * from which a 64-bit hash is computed. Unlike regular serialization, this
+ * approach excludes irrelevant information.
+ * 
+ * Use Cases:
  *
- * Normalization is implemented by fingerprinting queries, selectively
- * serializing those fields of each query tree's nodes that are judged to be
- * essential to the query.  This is referred to as a query jumble.  This is
- * distinct from a regular serialization in that various extraneous
- * information is ignored as irrelevant or not essential to the query, such
- * as the collations of Vars and, most notably, the values of constants.
- *
- * This jumble is acquired at the end of parse analysis of each query, and
- * a 64-bit hash of it is stored into the query's Query.queryId field.
- * The server then copies this value around, making it available in plan
- * tree(s) generated from the query.  The executor can then use this value
- * to blame query costs on the proper queryId.
- *
- * Arrays of two or more constants and PARAM_EXTERN parameters are "squashed"
- * and contribute only once to the jumble.  This has the effect that queries
- * that differ only on the length of such lists have the same queryId.
- *
- *
- * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
- * Portions Copyright (c) 1994, Regents of the University of California
+ * 1. In-Core Query Normalization & Identification
  *
+ * Fingerprinting is used to normalize query trees by generating a hash stored
+ * in the Query.queryId field. This ID is propagated to plan tree(s), allowing
+ * the executor to attribute query costs on the proper queryId. The process
+ * excludes information like typmod, collation, and most notably, the values
+ * of constants.
+ * 
+ * Example: The following queries produce the same queryId:
+ * 
+ * SELECT t.* FROM s1.t WHERE c1 = 1;
+ * SELECT t.* FROM s1.t WHERE c1 = 2;
+ * 
+ * 2. Modified jumbling logic for extensions
+ * 
+ * Extensions can modify the fingerprinting logic for queryId, or fingerprint
+ * other types of trees, such as a plan tree, to compute a plan identifier.
  *
  * IDENTIFICATION
  *	  src/backend/nodes/queryjumblefuncs.c
@@ -50,6 +49,7 @@
 
 /* GUC parameters */
 int			compute_query_id = COMPUTE_QUERY_ID_AUTO;
+int			compute_plan_id = COMPUTE_PLAN_ID_AUTO;
 
 /*
  * True when compute_query_id is ON or AUTO, and a module requests them.
@@ -60,15 +60,21 @@ int			compute_query_id = COMPUTE_QUERY_ID_AUTO;
  */
 bool		query_id_enabled = false;
 
-static JumbleState *InitJumble(void);
-static int64 DoJumble(JumbleState *jstate, Node *node);
+/*
+ * True when compute_plan_id is ON or AUTO, and a module requests them.
+ *
+ * Note that IsPlanIdEnabled() should be used instead of checking
+ * plan_id_enabled or compute_plan_id directly when we want to know
+ * whether query identifiers are computed in the core or not.
+ */
+bool		plan_id_enabled = false;
+
 static void AppendJumble(JumbleState *jstate,
 						 const unsigned char *value, Size size);
 static void FlushPendingNulls(JumbleState *jstate);
 static void RecordConstLocation(JumbleState *jstate,
 								bool extern_param,
 								int location, int len);
-static void _jumbleNode(JumbleState *jstate, Node *node);
 static void _jumbleList(JumbleState *jstate, Node *node);
 static void _jumbleElements(JumbleState *jstate, List *elements, Node *node);
 static void _jumbleParam(JumbleState *jstate, Node *node);
@@ -136,13 +142,13 @@ CleanQuerytext(const char *query, int *location, int *len)
 JumbleState *
 JumbleQuery(Query *query)
 {
-	JumbleState *jstate;
+	JumbleState *jstate = InitializeJumbleState(true);;
 
 	Assert(IsQueryIdEnabled());
 
-	jstate = InitJumble();
-
-	query->queryId = DoJumble(jstate, (Node *) query);
+	/* Compute query ID and mark the Query node with it */
+	JumbleNode(jstate, (Node *) query);
+	query->queryId = HashJumbleState(jstate);
 
 	/*
 	 * If we are unlucky enough to get a hash of zero, use 1 instead for
@@ -173,44 +179,44 @@ EnableQueryId(void)
 }
 
 /*
- * InitJumble
- *		Allocate a JumbleState object and make it ready to jumble.
+ * Enables plan identifier computation.
+ *
+ * Third-party plugins can use this function to inform core that they require
+ * a query identifier to be computed.
  */
-static JumbleState *
-InitJumble(void)
+void
+EnablePlanId(void)
 {
-	JumbleState *jstate;
+	if (compute_plan_id != COMPUTE_PLAN_ID_OFF)
+		plan_id_enabled = true;
+}
 
-	jstate = palloc_object(JumbleState);
+/*
+ * InitializeJumbleState
+ *		Allocate a JumbleState object and make it ready to jumble.
+ */
+JumbleState *
+InitializeJumbleState(bool record_clocations)
+{
+	JumbleState *jstate = (JumbleState *) palloc0(sizeof(JumbleState));
 
 	/* Set up workspace for query jumbling */
 	jstate->jumble = (unsigned char *) palloc(JUMBLE_SIZE);
 	jstate->jumble_len = 0;
-	jstate->clocations_buf_size = 32;
-	jstate->clocations = (LocationLen *) palloc(jstate->clocations_buf_size *
-												sizeof(LocationLen));
-	jstate->clocations_count = 0;
-	jstate->highest_extern_param_id = 0;
-	jstate->pending_nulls = 0;
-	jstate->has_squashed_lists = false;
-#ifdef USE_ASSERT_CHECKING
-	jstate->total_jumble_len = 0;
-#endif
+
+	if (record_clocations)
+	{
+		jstate->clocations_buf_size = 32;
+		jstate->clocations = (LocationLen *)
+			palloc(jstate->clocations_buf_size * sizeof(LocationLen));
+	}
 
 	return jstate;
 }
 
-/*
- * DoJumble
- *		Jumble the given Node using the given JumbleState and return the resulting
- *		jumble hash.
- */
-static int64
-DoJumble(JumbleState *jstate, Node *node)
+uint64
+HashJumbleState(JumbleState *jstate)
 {
-	/* Jumble the given node */
-	_jumbleNode(jstate, node);
-
 	/* Flush any pending NULLs before doing the final hash */
 	if (jstate->pending_nulls > 0)
 		FlushPendingNulls(jstate);
@@ -219,10 +225,9 @@ DoJumble(JumbleState *jstate, Node *node)
 	if (jstate->has_squashed_lists)
 		jstate->highest_extern_param_id = 0;
 
-	/* Process the jumble buffer and produce the hash value */
-	return DatumGetInt64(hash_any_extended(jstate->jumble,
-										   jstate->jumble_len,
-										   0));
+	return DatumGetUInt64(hash_any_extended(jstate->jumble,
+											jstate->jumble_len,
+											0));
 }
 
 /*
@@ -398,7 +403,7 @@ static void
 RecordConstLocation(JumbleState *jstate, bool extern_param, int location, int len)
 {
 	/* -1 indicates unknown or undefined location */
-	if (location >= 0)
+	if (location >= 0 && jstate->clocations_buf_size > 0)
 	{
 		/* enlarge array if needed */
 		if (jstate->clocations_count >= jstate->clocations_buf_size)
@@ -526,7 +531,7 @@ IsSquashableConstantList(List *elements)
 }
 
 #define JUMBLE_NODE(item) \
-	_jumbleNode(jstate, (Node *) expr->item)
+	JumbleNode(jstate, (Node *) expr->item)
 #define JUMBLE_ELEMENTS(list, node) \
 	_jumbleElements(jstate, (List *) expr->list, node)
 #define JUMBLE_LOCATION(location) \
@@ -544,6 +549,13 @@ do { \
 	else \
 		AppendJumble(jstate, (const unsigned char *) &(expr->item), sizeof(expr->item)); \
 } while (0)
+#define JUMBLE_BITMAPSET(item) \
+do { \
+	if (expr->item) \
+		AppendJumble(jstate, (const unsigned char *) expr->item->words, sizeof(bitmapword) * expr->item->nwords); \
+} while(0)
+#define JUMBLE_ARRAY(item, len) \
+	AppendJumble(jstate, (const unsigned char *) expr->item, sizeof(*(expr->item)) * len)
 #define JUMBLE_STRING(str) \
 do { \
 	if (expr->str) \
@@ -557,8 +569,8 @@ do { \
 
 #include "queryjumblefuncs.funcs.c"
 
-static void
-_jumbleNode(JumbleState *jstate, Node *node)
+void
+JumbleNode(JumbleState *jstate, Node *node)
 {
 	Node	   *expr = node;
 #ifdef USE_ASSERT_CHECKING
@@ -612,7 +624,7 @@ _jumbleList(JumbleState *jstate, Node *node)
 	{
 		case T_List:
 			foreach(l, expr)
-				_jumbleNode(jstate, lfirst(l));
+				JumbleNode(jstate, lfirst(l));
 			break;
 		case T_IntList:
 			foreach(l, expr)
@@ -668,7 +680,7 @@ _jumbleElements(JumbleState *jstate, List *elements, Node *node)
 
 	if (!normalize_list)
 	{
-		_jumbleNode(jstate, (Node *) elements);
+		JumbleNode(jstate, (Node *) elements);
 	}
 }
 
@@ -758,6 +770,40 @@ _jumbleVariableSetStmt(JumbleState *jstate, Node *node)
 	JUMBLE_LOCATION(location);
 }
 
+/*
+ * Jumble the entries in the rangle table to map RT indexes to relations
+ *
+ * This ensures jumbled RT indexes (e.g. in a Scan or Modify node), are
+ * distinguished by the target of the RT entry, even if the index is the same.
+ */
+void
+JumbleRangeTable(JumbleState *jstate, List *rtable)
+{
+	ListCell *lc;
+
+	foreach(lc, rtable)
+	{
+		RangeTblEntry *expr = lfirst_node(RangeTblEntry, lc);
+
+		switch (expr->rtekind)
+		{
+			case RTE_RELATION:
+				JUMBLE_FIELD(relid);
+				break;
+			case RTE_CTE:
+				JUMBLE_STRING(ctename);
+				break;
+			default:
+
+				/*
+				* Ignore other targets, the jumble includes something identifying
+				* about them already
+				*/
+				break;
+		}
+	}
+}
+
 /*
  * Custom query jumble function for RangeTblEntry.eref.
  */
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 8b22c30559b..0309566f602 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -37,6 +37,7 @@
 #ifdef OPTIMIZER_DEBUG
 #include "nodes/print.h"
 #endif
+#include "nodes/queryjumble.h"
 #include "nodes/supportnodes.h"
 #include "optimizer/appendinfo.h"
 #include "optimizer/clauses.h"
@@ -578,6 +579,16 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	Assert(glob->finalrowmarks == NIL);
 	Assert(glob->resultRelations == NIL);
 	Assert(glob->appendRelations == NIL);
+
+	/*
+	 * Initialize plan identifier jumble if needed
+	 *
+	 * Note the actual jumbling is done in the tree walk in
+	 * set_plan_references
+	 */
+	if (IsPlanIdEnabled())
+		glob->plan_jumble_state = InitializeJumbleState(false);
+
 	top_plan = set_plan_references(root, top_plan);
 	/* ... and the subplans (both regular subplans and initplans) */
 	Assert(list_length(glob->subplans) == list_length(glob->subroots));
@@ -645,6 +656,14 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 			result->jitFlags |= PGJIT_DEFORM;
 	}
 
+	if (IsPlanIdEnabled())
+	{
+		JumbleRangeTable(glob->plan_jumble_state, glob->finalrtable);
+		result->planId = HashJumbleState(glob->plan_jumble_state);
+		pfree(glob->plan_jumble_state->jumble);
+		pfree(glob->plan_jumble_state);
+	}
+
 	/* Allow plugins to take control before we discard "glob" */
 	if (planner_shutdown_hook)
 		(*planner_shutdown_hook) (glob, parse, query_string, result);
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index cd7ea1e6b58..947bd980c66 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -19,6 +19,7 @@
 #include "catalog/pg_type.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/queryjumble.h"
 #include "optimizer/optimizer.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/planmain.h"
@@ -1338,6 +1339,14 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
 	plan->lefttree = set_plan_refs(root, plan->lefttree, rtoffset);
 	plan->righttree = set_plan_refs(root, plan->righttree, rtoffset);
 
+	/*
+	 * If enabled, append significant information to the plan identifier
+	 * jumble (we do this here since we're already walking the tree in a
+	 * near-final state)
+	 */
+	if (IsPlanIdEnabled())
+		JumbleNode(root->glob->plan_jumble_state, (Node *) plan);
+
 	return plan;
 }
 
diff --git a/src/backend/postmaster/launch_backend.c b/src/backend/postmaster/launch_backend.c
index 98f7c4848c9..b64c88db200 100644
--- a/src/backend/postmaster/launch_backend.c
+++ b/src/backend/postmaster/launch_backend.c
@@ -118,6 +118,7 @@ typedef struct
 	bool		redirection_done;
 	bool		IsBinaryUpgrade;
 	bool		query_id_enabled;
+	bool		plan_id_enabled;
 	int			max_safe_fds;
 	int			MaxBackends;
 	int			num_pmchild_slots;
@@ -757,6 +758,7 @@ save_backend_variables(BackendParameters *param,
 	param->redirection_done = redirection_done;
 	param->IsBinaryUpgrade = IsBinaryUpgrade;
 	param->query_id_enabled = query_id_enabled;
+	param->plan_id_enabled = plan_id_enabled;
 	param->max_safe_fds = max_safe_fds;
 
 	param->MaxBackends = MaxBackends;
@@ -1019,6 +1021,7 @@ restore_backend_variables(BackendParameters *param)
 	redirection_done = param->redirection_done;
 	IsBinaryUpgrade = param->IsBinaryUpgrade;
 	query_id_enabled = param->query_id_enabled;
+	plan_id_enabled = param->plan_id_enabled;
 	max_safe_fds = param->max_safe_fds;
 
 	MaxBackends = param->MaxBackends;
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index ef6fffe60b9..854c9f8bffd 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -351,7 +351,7 @@ pg_stat_get_progress_info(PG_FUNCTION_ARGS)
 Datum
 pg_stat_get_activity(PG_FUNCTION_ARGS)
 {
-#define PG_STAT_GET_ACTIVITY_COLS	31
+#define PG_STAT_GET_ACTIVITY_COLS	32
 	int			num_backends = pgstat_fetch_stat_numbackends();
 	int			curr_backend;
 	int			pid = PG_ARGISNULL(0) ? -1 : PG_GETARG_INT32(0);
@@ -665,6 +665,10 @@ pg_stat_get_activity(PG_FUNCTION_ARGS)
 				nulls[30] = true;
 			else
 				values[30] = Int64GetDatum(beentry->st_query_id);
+			if (beentry->st_plan_id == INT64CONST(0))
+				nulls[31] = true;
+			else
+				values[31] = UInt64GetDatum(beentry->st_plan_id);
 		}
 		else
 		{
@@ -694,6 +698,7 @@ pg_stat_get_activity(PG_FUNCTION_ARGS)
 			nulls[28] = true;
 			nulls[29] = true;
 			nulls[30] = true;
+			nulls[31] = true;
 		}
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 3b9d8349078..d9f5cf31c34 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -462,6 +462,13 @@
   check_hook => 'check_commit_ts_buffers',
 },
 
+{ name => 'compute_plan_id', type => 'enum', context => 'PGC_SUSET', group => 'STATS_MONITORING',
+  short_desc => 'Enables in-core computation of plan identifiers.',
+  variable => 'compute_plan_id',
+  boot_val => 'COMPUTE_PLAN_ID_AUTO',
+  options => 'compute_plan_id_options',
+},
+
 { name => 'compute_query_id', type => 'enum', context => 'PGC_SUSET', group => 'STATS_MONITORING',
   short_desc => 'Enables in-core computation of query identifiers.',
   variable => 'compute_query_id',
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index f87b558c2c6..3a5d7ee32ab 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -318,6 +318,24 @@ static const struct config_enum_entry compute_query_id_options[] = {
 	{NULL, 0, false}
 };
 
+/*
+ * Although only "on" and "off" are documented, we accept
+ * all the likely variants of "on" and "off".
+ */
+static const struct config_enum_entry compute_plan_id_options[] = {
+	{"auto", COMPUTE_PLAN_ID_AUTO, false},
+	{"regress", COMPUTE_PLAN_ID_REGRESS, false},
+	{"on", COMPUTE_PLAN_ID_ON, false},
+	{"off", COMPUTE_PLAN_ID_OFF, false},
+	{"true", COMPUTE_PLAN_ID_ON, true},
+	{"false", COMPUTE_PLAN_ID_OFF, true},
+	{"yes", COMPUTE_PLAN_ID_ON, true},
+	{"no", COMPUTE_PLAN_ID_OFF, true},
+	{"1", COMPUTE_PLAN_ID_ON, true},
+	{"0", COMPUTE_PLAN_ID_OFF, true},
+	{NULL, 0, false}
+};
+
 /*
  * Although only "on", "off", and "partition" are documented, we
  * accept all the likely variants of "on" and "off".
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index dc9e2255f8a..3d0e42f083b 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -674,6 +674,7 @@
 # - Monitoring -
 
 #compute_query_id = auto
+#compute_plan_id = auto
 #log_statement_stats = off
 #log_parser_stats = off
 #log_planner_stats = off
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fd9448ec7b9..2bf284e2732 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5653,9 +5653,9 @@
   proname => 'pg_stat_get_activity', prorows => '100', proisstrict => 'f',
   proretset => 't', provolatile => 's', proparallel => 'r',
   prorettype => 'record', proargtypes => 'int4',
-  proallargtypes => '{int4,oid,int4,oid,text,text,text,text,text,timestamptz,timestamptz,timestamptz,timestamptz,inet,text,int4,xid,xid,text,bool,text,text,int4,text,numeric,text,bool,text,bool,bool,int4,int8}',
-  proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
-  proargnames => '{pid,datid,pid,usesysid,application_name,state,query,wait_event_type,wait_event,xact_start,query_start,backend_start,state_change,client_addr,client_hostname,client_port,backend_xid,backend_xmin,backend_type,ssl,sslversion,sslcipher,sslbits,ssl_client_dn,ssl_client_serial,ssl_issuer_dn,gss_auth,gss_princ,gss_enc,gss_delegation,leader_pid,query_id}',
+  proallargtypes => '{int4,oid,int4,oid,text,text,text,text,text,timestamptz,timestamptz,timestamptz,timestamptz,inet,text,int4,xid,xid,text,bool,text,text,int4,text,numeric,text,bool,text,bool,bool,int4,int8,int8}',
+  proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{pid,datid,pid,usesysid,application_name,state,query,wait_event_type,wait_event,xact_start,query_start,backend_start,state_change,client_addr,client_hostname,client_port,backend_xid,backend_xmin,backend_type,ssl,sslversion,sslcipher,sslbits,ssl_client_dn,ssl_client_serial,ssl_issuer_dn,gss_auth,gss_princ,gss_enc,gss_delegation,leader_pid,query_id,plan_id}',
   prosrc => 'pg_stat_get_activity' },
 { oid => '6318', descr => 'describe wait events',
   proname => 'pg_get_wait_events', procost => '10', prorows => '250',
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index b5ff456ef7f..c98f7b97283 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -189,6 +189,9 @@ typedef struct PlannerGlobal
 	/* extension state */
 	void	  **extension_state pg_node_attr(read_write_ignore);
 	int			extension_state_allocated;
+
+	/* optional jumble state for plan identifier calculation */
+	struct JumbleState *plan_jumble_state pg_node_attr(read_write_ignore);
 } PlannerGlobal;
 
 /* macro for fetching the Plan associated with a SubPlan node */
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index c4393a94321..a5a2ccea1f7 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -70,7 +70,7 @@ typedef struct PlannedStmt
 	/* query identifier (copied from Query) */
 	int64		queryId;
 
-	/* plan identifier (can be set by plugins) */
+	/* plan identifier (set when compute_plan_id is enabled or by plugins) */
 	int64		planId;
 
 	/* origin of plan */
@@ -184,7 +184,7 @@ typedef struct PlannedStmt
  */
 typedef struct Plan
 {
-	pg_node_attr(abstract, no_equal, no_query_jumble)
+	pg_node_attr(abstract, no_equal)
 
 	NodeTag		type;
 
@@ -192,19 +192,19 @@ typedef struct Plan
 	 * estimated execution costs for plan (see costsize.c for more info)
 	 */
 	/* count of disabled nodes */
-	int			disabled_nodes;
+	int			disabled_nodes pg_node_attr(query_jumble_ignore);
 	/* cost expended before fetching any tuples */
-	Cost		startup_cost;
+	Cost		startup_cost pg_node_attr(query_jumble_ignore);
 	/* total cost (assuming all tuples fetched) */
-	Cost		total_cost;
+	Cost		total_cost pg_node_attr(query_jumble_ignore);
 
 	/*
 	 * planner's estimate of result size of this plan step
 	 */
 	/* number of rows plan is expected to emit */
-	Cardinality plan_rows;
+	Cardinality plan_rows pg_node_attr(query_jumble_ignore);
 	/* average row width in bytes */
-	int			plan_width;
+	int			plan_width pg_node_attr(query_jumble_ignore);
 
 	/*
 	 * information needed for parallel query
@@ -230,10 +230,10 @@ typedef struct Plan
 	/* implicitly-ANDed qual conditions */
 	List	   *qual;
 	/* input plan tree(s) */
-	struct Plan *lefttree;
-	struct Plan *righttree;
+	struct Plan *lefttree pg_node_attr(query_jumble_ignore);
+	struct Plan *righttree pg_node_attr(query_jumble_ignore);
 	/* Init Plan nodes (un-correlated expr subselects) */
-	List	   *initPlan;
+	List	   *initPlan pg_node_attr(query_jumble_ignore);
 
 	/*
 	 * Information for management of parameter-change-driven rescanning
@@ -351,7 +351,7 @@ typedef struct ModifyTable
 	/* per-target-table RETURNING tlists */
 	List	   *returningLists;
 	/* per-target-table FDW private data lists */
-	List	   *fdwPrivLists;
+	List	   *fdwPrivLists pg_node_attr(query_jumble_ignore);
 	/* indices of FDW DM plans */
 	Bitmapset  *fdwDirectModifyPlans;
 	/* PlanRowMarks (non-locking only) */
@@ -390,7 +390,7 @@ typedef struct Append
 	Plan		plan;
 	/* RTIs of appendrel(s) formed by this node */
 	Bitmapset  *apprelids;
-	List	   *appendplans;
+	List	   *appendplans pg_node_attr(query_jumble_ignore);
 	/* # of asynchronous plans */
 	int			nasyncplans;
 
@@ -420,7 +420,7 @@ typedef struct MergeAppend
 	/* RTIs of appendrel(s) formed by this node */
 	Bitmapset  *apprelids;
 
-	List	   *mergeplans;
+	List	   *mergeplans pg_node_attr(query_jumble_ignore);
 
 	/* these fields are just like the sort-key info in struct Sort: */
 
@@ -475,7 +475,7 @@ typedef struct RecursiveUnion
 	Oid		   *dupCollations pg_node_attr(array_size(numCols));
 
 	/* estimated number of groups in input */
-	Cardinality numGroups;
+	long		numGroups pg_node_attr(query_jumble_ignore);
 } RecursiveUnion;
 
 /* ----------------
@@ -489,7 +489,7 @@ typedef struct RecursiveUnion
 typedef struct BitmapAnd
 {
 	Plan		plan;
-	List	   *bitmapplans;
+	List	   *bitmapplans pg_node_attr(query_jumble_ignore);
 } BitmapAnd;
 
 /* ----------------
@@ -504,7 +504,7 @@ typedef struct BitmapOr
 {
 	Plan		plan;
 	bool		isshared;
-	List	   *bitmapplans;
+	List	   *bitmapplans pg_node_attr(query_jumble_ignore);
 } BitmapOr;
 
 /*
@@ -752,7 +752,7 @@ typedef enum SubqueryScanStatus
 typedef struct SubqueryScan
 {
 	Scan		scan;
-	Plan	   *subplan;
+	Plan	   *subplan pg_node_attr(query_jumble_ignore);
 	SubqueryScanStatus scanstatus;
 } SubqueryScan;
 
@@ -880,7 +880,7 @@ typedef struct ForeignScan
 	/* expressions that FDW may evaluate */
 	List	   *fdw_exprs;
 	/* private data for FDW */
-	List	   *fdw_private;
+	List	   *fdw_private pg_node_attr(query_jumble_ignore);
 	/* optional tlist describing scan tuple */
 	List	   *fdw_scan_tlist;
 	/* original quals not in scan.plan.qual */
@@ -918,7 +918,7 @@ typedef struct CustomScan
 	/* expressions that custom code may evaluate */
 	List	   *custom_exprs;
 	/* private data for custom code */
-	List	   *custom_private;
+	List	   *custom_private pg_node_attr(query_jumble_ignore);
 	/* optional tlist describing scan tuple */
 	List	   *custom_scan_tlist;
 	/* RTIs generated by this scan */
@@ -929,7 +929,7 @@ typedef struct CustomScan
 	 * static table of callback functions.  So we don't copy the table itself,
 	 * just reference the original one.
 	 */
-	const struct CustomScanMethods *methods;
+	const struct CustomScanMethods *methods pg_node_attr(query_jumble_ignore);
 } CustomScan;
 
 /*
@@ -991,7 +991,7 @@ typedef struct NestLoop
 
 typedef struct NestLoopParam
 {
-	pg_node_attr(no_equal, no_query_jumble)
+	pg_node_attr(no_equal)
 
 	NodeTag		type;
 	/* number of the PARAM_EXEC Param to set */
@@ -1099,7 +1099,7 @@ typedef struct Memoize
 	 * The maximum number of entries that the planner expects will fit in the
 	 * cache, or 0 if unknown
 	 */
-	uint32		est_entries;
+	uint32		est_entries pg_node_attr(query_jumble_ignore);
 
 	/* paramids from param_exprs */
 	Bitmapset  *keyparamids;
@@ -1206,7 +1206,7 @@ typedef struct Agg
 	Oid		   *grpCollations pg_node_attr(array_size(numCols));
 
 	/* estimated number of groups in input */
-	Cardinality numGroups;
+	long		numGroups pg_node_attr(query_jumble_ignore);
 
 	/* for pass-by-ref transition data */
 	uint64		transitionSpace;
@@ -1415,7 +1415,7 @@ typedef struct Hash
 	bool		skewInherit;
 	/* all other info is in the parent HashJoin node */
 	/* estimate total rows if parallel_aware */
-	Cardinality rows_total;
+	Cardinality rows_total pg_node_attr(query_jumble_ignore);
 } Hash;
 
 /* ----------------
@@ -1446,7 +1446,7 @@ typedef struct SetOp
 	bool	   *cmpNullsFirst pg_node_attr(array_size(numCols));
 
 	/* estimated number of groups in left input */
-	Cardinality numGroups;
+	long		numGroups pg_node_attr(query_jumble_ignore);
 } SetOp;
 
 /* ----------------
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 1b4436f2ff6..4ccba6bcd03 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -1123,8 +1123,8 @@ typedef struct SubPlan
 	List	   *parParam;		/* indices of input Params from parent plan */
 	List	   *args;			/* exprs to pass as parParam values */
 	/* Estimated execution costs: */
-	Cost		startup_cost;	/* one-time setup cost */
-	Cost		per_call_cost;	/* cost for each subplan evaluation */
+	Cost		startup_cost pg_node_attr(query_jumble_ignore);	/* one-time setup cost */
+	Cost		per_call_cost pg_node_attr(query_jumble_ignore);	/* cost for each subplan evaluation */
 } SubPlan;
 
 /*
diff --git a/src/include/nodes/queryjumble.h b/src/include/nodes/queryjumble.h
index dcb36dcb44f..aeb20aa0587 100644
--- a/src/include/nodes/queryjumble.h
+++ b/src/include/nodes/queryjumble.h
@@ -15,6 +15,7 @@
 #define QUERYJUMBLE_H
 
 #include "nodes/parsenodes.h"
+#include "nodes/pathnodes.h"
 
 /*
  * Struct for tracking locations/lengths of constants during normalization
@@ -86,15 +87,26 @@ enum ComputeQueryIdType
 	COMPUTE_QUERY_ID_REGRESS,
 };
 
+/* Values for the compute_plan_id GUC */
+enum ComputePlanIdType
+{
+	COMPUTE_PLAN_ID_OFF,
+	COMPUTE_PLAN_ID_ON,
+	COMPUTE_PLAN_ID_AUTO,
+	COMPUTE_PLAN_ID_REGRESS,
+};
+
 /* GUC parameters */
 extern PGDLLIMPORT int compute_query_id;
-
+extern PGDLLIMPORT int compute_plan_id;
 
 extern const char *CleanQuerytext(const char *query, int *location, int *len);
 extern JumbleState *JumbleQuery(Query *query);
 extern void EnableQueryId(void);
+extern void EnablePlanId(void);
 
 extern PGDLLIMPORT bool query_id_enabled;
+extern PGDLLIMPORT bool plan_id_enabled;
 
 /*
  * Returns whether query identifier computation has been enabled, either
@@ -110,4 +122,25 @@ IsQueryIdEnabled(void)
 	return query_id_enabled;
 }
 
+/*
+ * Returns whether plan identifier computation has been enabled, either
+ * directly in the GUC or by a module when the setting is 'auto'.
+ */
+static inline bool
+IsPlanIdEnabled(void)
+{
+	if (compute_plan_id == COMPUTE_PLAN_ID_OFF)
+		return false;
+	if (compute_plan_id == COMPUTE_PLAN_ID_ON)
+		return true;
+	return plan_id_enabled;
+}
+
+/* Functions called for plan jumbling or extensions doing their own jumbling */
+extern JumbleState *InitializeJumbleState(bool record_clocations);
+//extern void AppendJumble(JumbleState *jstate, const unsigned char *item, Size size);
+extern void JumbleRangeTable(JumbleState *jstate, List *rtable);
+extern void JumbleNode(JumbleState *jstate, Node *node);
+extern uint64 HashJumbleState(JumbleState *jstate);
+
 #endif							/* QUERYJUMBLE_H */
diff --git a/src/include/utils/backend_status.h b/src/include/utils/backend_status.h
index 3016501ac05..45f00eb93a8 100644
--- a/src/include/utils/backend_status.h
+++ b/src/include/utils/backend_status.h
@@ -172,7 +172,7 @@ typedef struct PgBackendStatus
 	/* query identifier, optionally computed using post_parse_analyze_hook */
 	int64		st_query_id;
 
-	/* plan identifier, optionally computed using planner_hook */
+	/* plan identifier, optionally computed after planning */
 	int64		st_plan_id;
 } PgBackendStatus;
 
diff --git a/src/test/regress/expected/explain.out b/src/test/regress/expected/explain.out
index 7c1f26b182c..7ac025cca84 100644
--- a/src/test/regress/expected/explain.out
+++ b/src/test/regress/expected/explain.out
@@ -743,6 +743,17 @@ select explain_filter('explain (verbose) create table test_ctas as select 1');
  Query Identifier: N
 (3 rows)
 
+-- Test compute_plan_id
+set compute_plan_id = on;
+select explain_filter('explain (verbose) select * from int8_tbl i8');
+                         explain_filter                         
+----------------------------------------------------------------
+ Seq Scan on public.int8_tbl i8  (cost=N.N..N.N rows=N width=N)
+   Output: q1, q2
+ Query Identifier: N
+ Plan Identifier: N
+(4 rows)
+
 -- Test SERIALIZE option
 select explain_filter('explain (analyze,buffers off,serialize) select * from int8_tbl i8');
                                          explain_filter                                          
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 4286c266e17..29af9bd2de9 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1794,9 +1794,10 @@ pg_stat_activity| SELECT s.datid,
     s.backend_xid,
     s.backend_xmin,
     s.query_id,
+	s.plan_id,
     s.query,
     s.backend_type
-   FROM ((pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id)
+   FROM ((pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id, plan_id)
      LEFT JOIN pg_database d ON ((s.datid = d.oid)))
      LEFT JOIN pg_authid u ON ((s.usesysid = u.oid)));
 pg_stat_all_indexes| SELECT c.oid AS relid,
@@ -1926,7 +1927,7 @@ pg_stat_gssapi| SELECT pid,
     gss_princ AS principal,
     gss_enc AS encrypted,
     gss_delegation AS credentials_delegated
-   FROM pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id)
+   FROM pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id, plan_id)
   WHERE (client_port IS NOT NULL);
 pg_stat_io| SELECT backend_type,
     object,
@@ -2156,7 +2157,7 @@ pg_stat_replication| SELECT s.pid,
     w.sync_priority,
     w.sync_state,
     w.reply_time
-   FROM ((pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id)
+   FROM ((pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id, plan_id)
      JOIN pg_stat_get_wal_senders() w(pid, state, sent_lsn, write_lsn, flush_lsn, replay_lsn, write_lag, flush_lag, replay_lag, sync_priority, sync_state, reply_time) ON ((s.pid = w.pid)))
      LEFT JOIN pg_authid u ON ((s.usesysid = u.oid)));
 pg_stat_replication_slots| SELECT s.slot_name,
@@ -2193,7 +2194,7 @@ pg_stat_ssl| SELECT pid,
     ssl_client_dn AS client_dn,
     ssl_client_serial AS client_serial,
     ssl_issuer_dn AS issuer_dn
-   FROM pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id)
+   FROM pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id, plan_id)
   WHERE (client_port IS NOT NULL);
 pg_stat_subscription| SELECT su.oid AS subid,
     su.subname,
diff --git a/src/test/regress/sql/explain.sql b/src/test/regress/sql/explain.sql
index ebdab42604b..0449f05c1e1 100644
--- a/src/test/regress/sql/explain.sql
+++ b/src/test/regress/sql/explain.sql
@@ -173,6 +173,10 @@ select explain_filter('explain (verbose) select * from int8_tbl i8');
 select explain_filter('explain (verbose) declare test_cur cursor for select * from int8_tbl');
 select explain_filter('explain (verbose) create table test_ctas as select 1');
 
+-- Test compute_plan_id
+set compute_plan_id = on;
+select explain_filter('explain (verbose) select * from int8_tbl i8');
+
 -- Test SERIALIZE option
 select explain_filter('explain (analyze,buffers off,serialize) select * from int8_tbl i8');
 select explain_filter('explain (analyze,serialize text,buffers,timing off) select * from int8_tbl i8');

base-commit: b39013b7b1b116b5d9be51f0919b472b58b3a28d
-- 
2.43.0

