From 5b5d84f3d5a25ef47fa9042985a8dfe33c76473c Mon Sep 17 00:00:00 2001
From: Lukas Fittl <lukas@fittl.com>
Date: Tue, 31 Dec 2024 15:16:10 -0800
Subject: [PATCH 3/4] Optionally record a plan_id in PlannedStmt to identify
 plan shape

When enabled via the new compute_plan_id GUC (default off), this utilizes
the existing treewalk in setrefs.c after planning to calculate a hash
(the "plan_id", or plan identifier) that can be used to identify
which plan was chosen.

The plan_id generally intends to be the same if a given EXPLAIN (without
ANALYZE) output is the same. The plan_id includes both the top-level plan
as well as all subplans. Execution statistics are excluded.

If enabled, the plan_id is shown for currently running queries in
pg_stat_activity, as well as recorded in EXPLAIN and auto_explain output.

Other in core users or extensions can use this facility to show or
accumulate statistics about the plans used by queries, to help identify
plan regressions, or drive plan management decisions.

Note that this commit intentionally does not include a facility to map
a given plan_id to the EXPLAIN text output - it is a assumed that users
can utilize the auto_explain extension to establish this mapping as
needed, or extensions can record this via the existing planner hook.
---
 doc/src/sgml/config.sgml                      |  32 ++
 doc/src/sgml/monitoring.sgml                  |  16 +
 src/backend/catalog/system_views.sql          |   1 +
 src/backend/commands/explain.c                |  17 +
 src/backend/executor/execMain.c               |   8 +-
 src/backend/executor/execParallel.c           |   1 +
 src/backend/nodes/Makefile                    |   1 +
 src/backend/nodes/meson.build                 |   1 +
 src/backend/nodes/planjumble.c                | 451 ++++++++++++++++++
 src/backend/optimizer/plan/planner.c          |  17 +
 src/backend/optimizer/plan/setrefs.c          |   8 +
 src/backend/tcop/postgres.c                   |   1 +
 src/backend/utils/activity/backend_status.c   |  70 ++-
 src/backend/utils/adt/pgstatfuncs.c           |   7 +-
 src/backend/utils/misc/guc_tables.c           |  28 ++
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/catalog/pg_proc.dat               |   6 +-
 src/include/nodes/pathnodes.h                 |   3 +
 src/include/nodes/planjumble.h                |  33 ++
 src/include/nodes/plannodes.h                 |   4 +-
 src/include/nodes/primnodes.h                 |   6 +-
 src/include/utils/backend_status.h            |   5 +
 src/test/regress/expected/explain.out         |  11 +
 src/test/regress/expected/rules.out           |   9 +-
 src/test/regress/sql/explain.sql              |   4 +
 25 files changed, 722 insertions(+), 19 deletions(-)
 create mode 100644 src/backend/nodes/planjumble.c
 create mode 100644 src/include/nodes/planjumble.h

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index fbdd6ce574..196d64efd8 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8546,6 +8546,38 @@ 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) 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>off</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 d0d176cc54..85b38ee642 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -972,6 +972,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 da9a8fe99f..a26e00a792 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -880,6 +880,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 a201ed3082..deeedf70cc 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -25,6 +25,7 @@
 #include "nodes/extensible.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/planjumble.h"
 #include "parser/analyze.h"
 #include "parser/parsetree.h"
 #include "rewrite/rewriteHandler.h"
@@ -966,6 +967,22 @@ ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc)
 		ExplainPropertyInteger("Query Identifier", NULL, (int64)
 							   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, (int64)
+							   queryDesc->plannedstmt->planId, es);
+	}
 }
 
 /*
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 1c12d6ebff..19becdd66c 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -120,13 +120,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
+	 * Note that it's harmless to report the identifiers multiple times, as the
+	 * call will be ignored if the top level query_id / plan_id has already been
 	 * reported.
 	 */
 	pgstat_report_query_id(queryDesc->plannedstmt->queryId, false);
+	pgstat_report_plan_id(queryDesc->plannedstmt->planId, queryDesc->plannedstmt->queryId, false);
 
 	if (ExecutorStart_hook)
 		(*ExecutorStart_hook) (queryDesc, eflags);
diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.c
index 846ec727de..54cb22967e 100644
--- a/src/backend/executor/execParallel.c
+++ b/src/backend/executor/execParallel.c
@@ -174,6 +174,7 @@ ExecSerializePlan(Plan *plan, EState *estate)
 	pstmt = makeNode(PlannedStmt);
 	pstmt->commandType = CMD_SELECT;
 	pstmt->queryId = pgstat_get_my_query_id();
+	pstmt->planId = pgstat_get_my_plan_id();
 	pstmt->hasReturning = false;
 	pstmt->hasModifyingCTE = false;
 	pstmt->canSetTag = true;
diff --git a/src/backend/nodes/Makefile b/src/backend/nodes/Makefile
index 66bbad8e6e..1ab9345d28 100644
--- a/src/backend/nodes/Makefile
+++ b/src/backend/nodes/Makefile
@@ -25,6 +25,7 @@ OBJS = \
 	nodeFuncs.o \
 	outfuncs.o \
 	params.o \
+	planjumble.o \
 	print.o \
 	queryjumblefuncs.o \
 	read.o \
diff --git a/src/backend/nodes/meson.build b/src/backend/nodes/meson.build
index 49626f160e..4640e7a361 100644
--- a/src/backend/nodes/meson.build
+++ b/src/backend/nodes/meson.build
@@ -8,6 +8,7 @@ backend_sources += files(
   'multibitmapset.c',
   'nodeFuncs.c',
   'params.c',
+  'planjumble.c',
   'print.c',
   'read.c',
   'tidbitmap.c',
diff --git a/src/backend/nodes/planjumble.c b/src/backend/nodes/planjumble.c
new file mode 100644
index 0000000000..a2f580d771
--- /dev/null
+++ b/src/backend/nodes/planjumble.c
@@ -0,0 +1,451 @@
+/*-------------------------------------------------------------------------
+ *
+ * planjumble.c
+ *	 Plan fingerprinting.
+ *
+ * Calculates the plan fingerprint for a given plan tree. Note this works
+ * in combination with the planner's setrefs functionality in order to
+ * walk the tree.
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/nodes/planjumble.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "nodes/planjumble.h"
+#include "parser/parsetree.h"
+
+#define JUMBLE_SIZE				1024	/* query serialization buffer size */
+
+/* GUC parameters */
+int			compute_plan_id = COMPUTE_PLAN_ID_OFF;
+
+#define JUMBLE_VALUE(item) \
+	AppendJumble(jstate, (const unsigned char *) &(item), sizeof(item))
+#define JUMBLE_STRING(str) \
+do { \
+	if (str) \
+		AppendJumble(jstate, (const unsigned char *) (str), strlen(str) + 1); \
+} while(0)
+
+/*
+ * Jumble the target relation of a scan or modify node
+ *
+ * This functions similarly to ExplainTargetRel.
+ */
+static void
+JumbleTargetRel(JumbleState *jstate, List* rtable, Plan *plan, Index rti)
+{
+	RangeTblEntry *rte = rt_fetch(rti, rtable);
+
+	switch (nodeTag(plan))
+	{
+		case T_SeqScan:
+		case T_SampleScan:
+		case T_IndexScan:
+		case T_IndexOnlyScan:
+		case T_BitmapHeapScan:
+		case T_TidScan:
+		case T_TidRangeScan:
+		case T_ForeignScan:
+		case T_CustomScan:
+		case T_ModifyTable:
+			/* Assert it's on a real relation */
+			Assert(rte->rtekind == RTE_RELATION);
+			JUMBLE_VALUE(rte->relid);
+			break;
+		case T_TableFuncScan:
+			{
+				TableFunc  *tablefunc = ((TableFuncScan *) plan)->tablefunc;
+
+				Assert(rte->rtekind == RTE_TABLEFUNC);
+				JUMBLE_VALUE(tablefunc->functype);
+			}
+			break;
+		case T_ValuesScan:
+			Assert(rte->rtekind == RTE_VALUES);
+			break;
+		case T_CteScan:
+			/* Assert it's on a non-self-reference CTE */
+			Assert(rte->rtekind == RTE_CTE);
+			Assert(!rte->self_reference);
+			JUMBLE_STRING(rte->ctename);
+			break;
+		case T_NamedTuplestoreScan:
+			Assert(rte->rtekind == RTE_NAMEDTUPLESTORE);
+			JUMBLE_STRING(rte->enrname);
+			break;
+		case T_WorkTableScan:
+			/* Assert it's on a self-reference CTE */
+			Assert(rte->rtekind == RTE_CTE);
+			Assert(rte->self_reference);
+			JUMBLE_STRING(rte->ctename);
+			break;
+		default:
+			break;
+	}
+}
+
+/*
+ * Jumble the target of a Scan node
+ */
+static void
+JumbleScanTarget(JumbleState *jstate, List* rtable, Scan *scan)
+{
+	JumbleTargetRel(jstate, rtable, (Plan *) scan, scan->scanrelid);
+}
+
+/*
+ * JumblePlanNode: Append significant information to the plan identifier jumble
+ *
+ * Note this intentionally doesn't descend into child plan nodes, since the caller
+ * already takes care of that.
+ */
+void
+JumblePlanNode(JumbleState *jstate, List* rtable, Plan *plan)
+{
+	JUMBLE_VALUE(nodeTag(plan));
+	JumbleNode(jstate, (Node *) plan->qual);
+	JumbleNode(jstate, (Node *) plan->targetlist);
+
+	/*
+	 * Plan-type-specific fixes
+	 */
+	switch (nodeTag(plan))
+	{
+		case T_SeqScan:
+			{
+				SeqScan	*splan = (SeqScan *) plan;
+				JumbleScanTarget(jstate, rtable, &splan->scan);
+			}
+			break;
+		case T_SampleScan:
+			{
+				SampleScan *splan = (SampleScan *) plan;
+				JumbleScanTarget(jstate, rtable, &splan->scan);
+				/* TODO: It may be worth jumbling the properties of splan->tablesample */
+			}
+			break;
+		case T_IndexScan:
+			{
+				IndexScan  *splan = (IndexScan *) plan;
+				JumbleScanTarget(jstate, rtable, &splan->scan);
+				JUMBLE_VALUE(splan->indexid);
+				JumbleNode(jstate, (Node *) splan->indexqual);
+				/* Skip splan->indexqualorig */
+				JumbleNode(jstate, (Node *) splan->indexorderby);
+				/* Skip splan->indexorderbyorig */
+				JUMBLE_VALUE(splan->indexorderdir);
+			}
+			break;
+		case T_IndexOnlyScan:
+			{
+				IndexOnlyScan *splan = (IndexOnlyScan *) plan;
+				JumbleScanTarget(jstate, rtable, &splan->scan);
+				JUMBLE_VALUE(splan->indexid);
+				JumbleNode(jstate, (Node *) splan->indexqual);
+				/* Skip splan->recheckqual */
+				JumbleNode(jstate, (Node *) splan->indexorderby);
+				/* Skip splan->indextlist */
+				JUMBLE_VALUE(splan->indexorderdir);
+			}
+			break;
+		case T_BitmapIndexScan:
+			{
+				BitmapIndexScan *splan = (BitmapIndexScan *) plan;
+				JumbleScanTarget(jstate, rtable, &splan->scan);
+				JUMBLE_VALUE(splan->indexid);
+				JumbleNode(jstate, (Node *) splan->indexqual);
+				/* Skip splan->indexqualorig */
+			}
+			break;
+		case T_BitmapHeapScan:
+			{
+				BitmapHeapScan *splan = (BitmapHeapScan *) plan;
+				JumbleScanTarget(jstate, rtable, &splan->scan);
+				/* Skip splan->bitmapqualorig */
+			}
+			break;
+		case T_TidScan:
+			{
+				TidScan	*splan = (TidScan *) plan;
+				JumbleScanTarget(jstate, rtable, &splan->scan);
+				JumbleNode(jstate, (Node *) splan->tidquals);
+			}
+			break;
+		case T_TidRangeScan:
+			{
+				TidRangeScan *splan = (TidRangeScan *) plan;
+				JumbleScanTarget(jstate, rtable, &splan->scan);
+				JumbleNode(jstate, (Node *) splan->tidrangequals);
+			}
+			break;
+		case T_SubqueryScan:
+			{
+				SubqueryScan *splan = (SubqueryScan *) plan;
+				/* TODO: JumbleScanHeader currently doesn't jumble the subplan name */
+				JumbleScanTarget(jstate, rtable, &splan->scan);
+				/* We rely on the caller to descend into the actual subplans */
+			}
+			break;
+		case T_FunctionScan:
+			{
+				FunctionScan *splan = (FunctionScan *) plan;
+
+				/*
+				 * If the expression is still a call of a single function,
+				 * we can jumble the OID of the function. Otherwise, punt.
+				 * (Even if it was a single function call originally, the
+				 * optimizer could have simplified it away.)
+				 */
+				if (list_length(splan->functions) == 1)
+				{
+					RangeTblFunction *rtfunc = (RangeTblFunction *) linitial(splan->functions);
+
+					if (IsA(rtfunc->funcexpr, FuncExpr))
+					{
+						FuncExpr   *funcexpr = (FuncExpr *) rtfunc->funcexpr;
+						Oid			funcid = funcexpr->funcid;
+						JUMBLE_VALUE(funcid);
+					}
+				}
+			}
+			break;
+		case T_TableFuncScan:
+			{
+				TableFuncScan *splan = (TableFuncScan *) plan;
+				JumbleScanTarget(jstate, rtable, &splan->scan);
+				/* TODO: Should we jumble splan->tablefunc? */
+			}
+			break;
+		case T_ValuesScan:
+			{
+				ValuesScan *splan = (ValuesScan *) plan;
+				JumbleScanTarget(jstate, rtable, &splan->scan);
+				JumbleNode(jstate, (Node *) splan->values_lists);
+			}
+			break;
+		case T_CteScan:
+			{
+				CteScan	*splan = (CteScan *) plan;
+				JumbleScanTarget(jstate, rtable, &splan->scan);
+			}
+			break;
+		case T_NamedTuplestoreScan:
+			{
+				NamedTuplestoreScan *splan = (NamedTuplestoreScan *) plan;
+				JumbleScanTarget(jstate, rtable, &splan->scan);
+			}
+			break;
+		case T_WorkTableScan:
+			{
+				WorkTableScan *splan = (WorkTableScan *) plan;
+				JumbleScanTarget(jstate, rtable, &splan->scan);
+			}
+			break;
+		case T_ForeignScan:
+			{
+				ForeignScan *splan = (ForeignScan *) plan;
+				JumbleScanTarget(jstate, rtable, &splan->scan);
+				/* TODO: Should we jumble any FDW-specific information here, like EXPLAIN? */
+			}
+			break;
+		case T_CustomScan:
+			{
+				ForeignScan *splan = (ForeignScan *) plan;
+				JumbleScanTarget(jstate, rtable, &splan->scan);
+			}
+			break;
+		case T_NestLoop:
+			{
+				NestLoop *jplan = (NestLoop *) plan;
+				JUMBLE_VALUE(jplan->join.jointype);
+				/* Skip jplan->join.inner_unique */
+				JumbleNode(jstate, (Node *) jplan->join.joinqual);
+				JumbleNode(jstate, (Node *) jplan->nestParams);
+			}
+			break;
+		case T_MergeJoin:
+			{
+				MergeJoin *jplan = (MergeJoin *) plan;
+				JUMBLE_VALUE(jplan->join.jointype);
+				/* Skip jplan->join.inner_unique */
+				JumbleNode(jstate, (Node *) jplan->join.joinqual);
+				JumbleNode(jstate, (Node *) jplan->mergeclauses);
+				JumbleNode(jstate, (Node *) jplan->mergeFamilies);
+				JumbleNode(jstate, (Node *) jplan->mergeCollations);
+				JumbleNode(jstate, (Node *) jplan->mergeReversals);
+				JumbleNode(jstate, (Node *) jplan->mergeNullsFirst);
+			}
+			break;
+		case T_HashJoin:
+			{
+				HashJoin *jplan = (HashJoin *) plan;
+				JUMBLE_VALUE(jplan->join.jointype);
+				/* Skip jplan->join.inner_unique */
+				JumbleNode(jstate, (Node *) jplan->join.joinqual);
+				JumbleNode(jstate, (Node *) jplan->hashclauses);
+				JumbleNode(jstate, (Node *) jplan->hashoperators);
+				JumbleNode(jstate, (Node *) jplan->hashcollations);
+				JumbleNode(jstate, (Node *) jplan->hashkeys);
+			}
+			break;
+		case T_Gather:
+			{
+				Gather *gplan = (Gather *) plan;
+				JUMBLE_VALUE(gplan->num_workers);
+				/* Skip all other fields */
+			}
+			break;
+		case T_GatherMerge:
+			{
+				GatherMerge *gplan = (GatherMerge *) plan;
+				JUMBLE_VALUE(gplan->num_workers);
+				/* Skip all other fields */
+			}
+			break;
+		case T_Hash:
+			{
+				Hash *hplan = (Hash *) plan;
+				JumbleNode(jstate, (Node *) hplan->hashkeys);
+				/* Skip all other fields */
+				break;
+			}
+			break;
+		case T_Memoize:
+			{
+				Memoize	*mplan = (Memoize *) plan;
+				JumbleNode(jstate, (Node *) mplan->param_exprs);
+				JUMBLE_VALUE(mplan->binary_mode);
+				/* Skip all other fields */
+			}
+			break;
+		case T_Material:
+			/* Materialize node has no fields of its own */
+			break;
+		case T_Sort:
+			{
+				Sort *splan = (Sort *) plan;
+				int i;
+				for (i = 0; i < splan->numCols; i++)
+				{
+					JUMBLE_VALUE(splan->sortColIdx[i]);
+					JUMBLE_VALUE(splan->sortOperators[i]);
+					JUMBLE_VALUE(splan->collations[i]);
+					JUMBLE_VALUE(splan->nullsFirst[i]);
+				}
+			}
+			break;
+		case T_IncrementalSort:
+			{
+				IncrementalSort *splan = (IncrementalSort *) plan;
+				int i;
+				for (i = 0; i < splan->sort.numCols; i++)
+				{
+					JUMBLE_VALUE(splan->sort.sortColIdx[i]);
+					JUMBLE_VALUE(splan->sort.sortOperators[i]);
+					JUMBLE_VALUE(splan->sort.collations[i]);
+					JUMBLE_VALUE(splan->sort.nullsFirst[i]);
+				}
+				JUMBLE_VALUE(splan->nPresortedCols);
+			}
+			break;
+		case T_Unique:
+			/* Skip all Unique node fields since EXPLAIN does not show them either */
+			break;
+		case T_SetOp:
+			{
+				SetOp *splan = (SetOp *) plan;
+				JUMBLE_VALUE(splan->cmd);
+				JUMBLE_VALUE(splan->strategy);
+				/* Skip all other fields since EXPLAIN does not show them either */
+			}
+			break;
+		case T_LockRows:
+			/* Skip all LockRows node fields since EXPLAIN does not show them either */
+			break;
+		case T_Limit:
+			/* Skip all Limit node fields since EXPLAIN does not show them either */
+			break;
+		case T_Agg:
+			{
+				Agg *agg = (Agg *) plan;
+				JUMBLE_VALUE(agg->aggstrategy);
+				JUMBLE_VALUE(agg->aggsplit);
+				/* Skip all other fields since EXPLAIN does not show them either */
+			}
+			break;
+		case T_Group:
+			{
+				Group *gplan = (Group *) plan;
+				int i;
+				for (i = 0; i < gplan->numCols; i++)
+				{
+					JUMBLE_VALUE(gplan->grpColIdx[i]);
+					JUMBLE_VALUE(gplan->grpOperators[i]);
+					JUMBLE_VALUE(gplan->grpCollations[i]);
+				}
+			}
+			break;
+		case T_WindowAgg:
+			{
+				WindowAgg *wplan = (WindowAgg *) plan;
+				JumbleNode(jstate, (Node *) wplan->runConditionOrig);
+				/* Skip all other fields since EXPLAIN does not show them either */
+			}
+			break;
+		case T_Result:
+			{
+				Result *splan = (Result *) plan;
+				JumbleNode(jstate, splan->resconstantqual);
+			}
+			break;
+		case T_ProjectSet:
+			/* ProjectSet node has no fields of its own */
+			break;
+		case T_ModifyTable:
+			{
+				ModifyTable *splan = (ModifyTable *) plan;
+				ListCell *lc;
+				JUMBLE_VALUE(splan->operation);
+				foreach (lc, splan->resultRelations)
+				{
+					JumbleTargetRel(jstate, rtable, plan, lfirst_int(lc));
+				}
+				JUMBLE_VALUE(splan->onConflictAction);
+				foreach (lc, splan->arbiterIndexes)
+				{
+					JumbleTargetRel(jstate, rtable, plan, lfirst_int(lc));
+				}
+				JumbleNode(jstate, splan->onConflictWhere);
+				/* Skip all other fields since EXPLAIN does not show them either */
+			}
+			break;
+		case T_Append:
+			/* Descending into Append node children is handled by the caller */
+			break;
+		case T_MergeAppend:
+			/* Descending into MergeAppend node children is handled by the caller */
+			break;
+		case T_RecursiveUnion:
+			/* Skip all RecursiveUnion node fields since EXPLAIN does not show them either */
+			break;
+		case T_BitmapAnd:
+			/* Descending into BitmapAnd node children is handled by the caller */
+			break;
+		case T_BitmapOr:
+			/* Descending into BitmapOr node children is handled by the caller */
+			break;
+		default:
+			elog(ERROR, "unrecognized node type: %d",
+				 (int) nodeTag(plan));
+			break;
+	}
+}
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 7468961b01..16888d152e 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -34,6 +34,7 @@
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
+#include "nodes/planjumble.h"
 #ifdef OPTIMIZER_DEBUG
 #include "nodes/print.h"
 #endif
@@ -532,6 +533,15 @@ 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 (compute_plan_id == COMPUTE_PLAN_ID_ON)
+		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));
@@ -570,6 +580,13 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	result->stmt_location = parse->stmt_location;
 	result->stmt_len = parse->stmt_len;
 
+	if (compute_plan_id == COMPUTE_PLAN_ID_ON)
+	{
+		result->planId = HashJumbleState(glob->plan_jumble_state);
+		pfree(glob->plan_jumble_state->jumble);
+		pfree(glob->plan_jumble_state);
+	}
+
 	result->jitFlags = PGJIT_NONE;
 	if (jit_enabled && jit_above_cost >= 0 &&
 		top_plan->total_cost > jit_above_cost)
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 6d23df108d..c02f939690 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/planjumble.h"
 #include "optimizer/optimizer.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/planmain.h"
@@ -1295,6 +1296,13 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
 			break;
 	}
 
+	/*
+	 * 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 (compute_plan_id == COMPUTE_PLAN_ID_ON)
+		JumblePlanNode(root->glob->plan_jumble_state, root->glob->finalrtable, plan);
+
 	/*
 	 * Now recurse into child plans, if any
 	 *
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 8590278818..03a04a198e 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -1106,6 +1106,7 @@ exec_simple_query(const char *query_string)
 		size_t		cmdtaglen;
 
 		pgstat_report_query_id(0, true);
+		pgstat_report_plan_id(0, 0, true);
 
 		/*
 		 * Get the command name for use in status display (it also becomes the
diff --git a/src/backend/utils/activity/backend_status.c b/src/backend/utils/activity/backend_status.c
index bf33e33a4e..77f3ca14ff 100644
--- a/src/backend/utils/activity/backend_status.c
+++ b/src/backend/utils/activity/backend_status.c
@@ -379,6 +379,7 @@ pgstat_bestart(void)
 	lbeentry.st_progress_command = PROGRESS_COMMAND_INVALID;
 	lbeentry.st_progress_command_target = InvalidOid;
 	lbeentry.st_query_id = UINT64CONST(0);
+	lbeentry.st_plan_id = UINT64CONST(0);
 
 	/*
 	 * we don't zero st_progress_param here to save cycles; nobody should
@@ -533,6 +534,7 @@ pgstat_report_activity(BackendState state, const char *cmd_str)
 			/* st_xact_start_timestamp and wait_event_info are also disabled */
 			beentry->st_xact_start_timestamp = 0;
 			beentry->st_query_id = UINT64CONST(0);
+			beentry->st_plan_id = UINT64CONST(0);
 			proc->wait_event_info = 0;
 			PGSTAT_END_WRITE_ACTIVITY(beentry);
 		}
@@ -588,12 +590,15 @@ pgstat_report_activity(BackendState state, const char *cmd_str)
 	beentry->st_state_start_timestamp = current_timestamp;
 
 	/*
-	 * If a new query is started, we reset the query identifier as it'll only
-	 * be known after parse analysis, to avoid reporting last query's
-	 * identifier.
+	 * If a new query is started, we reset the query and plan identifier as it'll only
+	 * be known after parse analysis / planning, to avoid reporting last query's
+	 * identifiers.
 	 */
 	if (state == STATE_RUNNING)
+	{
 		beentry->st_query_id = UINT64CONST(0);
+		beentry->st_plan_id = UINT64CONST(0);
+	}
 
 	if (cmd_str != NULL)
 	{
@@ -644,6 +649,45 @@ pgstat_report_query_id(uint64 query_id, bool force)
 	PGSTAT_END_WRITE_ACTIVITY(beentry);
 }
 
+/* --------
+ * pgstat_report_plan_id() -
+ *
+ * Called to update top-level plan identifier.
+ * --------
+ */
+void
+pgstat_report_plan_id(uint64 plan_id, uint64 query_id, bool force)
+{
+	volatile PgBackendStatus *beentry = MyBEEntry;
+
+	/*
+	 * if track_activities is disabled, st_plan_id should already have been
+	 * reset
+	 */
+	if (!beentry || !pgstat_track_activities)
+		return;
+
+	/*
+	 * We only report the top-level plan identifiers.  The stored plan_id is
+	 * reset when a backend calls pgstat_report_activity(STATE_RUNNING), or
+	 * with an explicit call to this function using the force flag.  If the
+	 * saved plan identifier is not zero or the query identifier is 0,
+	 * it means that it's not a top-level command, so ignore the one provided
+	 * unless it's an explicit call to reset the identifier.
+	 */
+	if ((beentry->st_plan_id != 0 || query_id == 0) && !force)
+		return;
+
+	/*
+	 * Update my status entry, following the protocol of bumping
+	 * st_changecount before and after.  We use a volatile pointer here to
+	 * ensure the compiler doesn't try to get cute.
+	 */
+	PGSTAT_BEGIN_WRITE_ACTIVITY(beentry);
+	beentry->st_plan_id = plan_id;
+	PGSTAT_END_WRITE_ACTIVITY(beentry);
+}
+
 
 /* ----------
  * pgstat_report_appname() -
@@ -1040,6 +1084,26 @@ pgstat_get_my_query_id(void)
 	return MyBEEntry->st_query_id;
 }
 
+/* ----------
+ * pgstat_get_my_plan_id() -
+ *
+ * Return current backend's plan identifier.
+ */
+uint64
+pgstat_get_my_plan_id(void)
+{
+	if (!MyBEEntry)
+		return 0;
+
+	/*
+	 * There's no need for a lock around pgstat_begin_read_activity /
+	 * pgstat_end_read_activity here as it's only called from
+	 * pg_stat_get_activity which is already protected, or from the same
+	 * backend which means that there won't be concurrent writes.
+	 */
+	return MyBEEntry->st_plan_id;
+}
+
 /* ----------
  * pgstat_get_backend_type_by_proc_number() -
  *
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 6fc34f7494..4a5fc94877 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -302,7 +302,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);
@@ -613,6 +613,10 @@ pg_stat_get_activity(PG_FUNCTION_ARGS)
 				nulls[30] = true;
 			else
 				values[30] = UInt64GetDatum(beentry->st_query_id);
+			if (beentry->st_plan_id == 0)
+				nulls[31] = true;
+			else
+				values[31] = UInt64GetDatum(beentry->st_plan_id);
 		}
 		else
 		{
@@ -642,6 +646,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_tables.c b/src/backend/utils/misc/guc_tables.c
index 8cf1afbad2..5139ba094a 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -50,6 +50,7 @@
 #include "libpq/auth.h"
 #include "libpq/libpq.h"
 #include "libpq/scram.h"
+#include "nodes/planjumble.h"
 #include "nodes/queryjumble.h"
 #include "optimizer/cost.h"
 #include "optimizer/geqo.h"
@@ -309,6 +310,23 @@ 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[] = {
+	{"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".
@@ -4863,6 +4881,16 @@ struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"compute_plan_id", PGC_SUSET, STATS_MONITORING,
+			gettext_noop("Enables in-core computation of plan identifiers."),
+			NULL
+		},
+		&compute_plan_id,
+		COMPUTE_PLAN_ID_OFF, compute_plan_id_options,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"constraint_exclusion", PGC_USERSET, QUERY_TUNING_OTHER,
 			gettext_noop("Enables the planner to use constraints to optimize queries."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index a2ac7575ca..70fa7f886f 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -649,6 +649,7 @@
 # - Monitoring -
 
 #compute_query_id = auto
+#compute_plan_id = off
 #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 2dcc2d42da..56feae2583 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5549,9 +5549,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 58748d2ca6..9068cb826e 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -163,6 +163,9 @@ typedef struct PlannerGlobal
 
 	/* partition descriptors */
 	PartitionDirectory partition_directory pg_node_attr(read_write_ignore);
+
+	/* optional jumble state for plan identifier claculation */
+	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/planjumble.h b/src/include/nodes/planjumble.h
new file mode 100644
index 0000000000..d35b66aec4
--- /dev/null
+++ b/src/include/nodes/planjumble.h
@@ -0,0 +1,33 @@
+/*-------------------------------------------------------------------------
+ *
+ * planjumble.h
+ *	  Plan fingerprinting.
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/include/nodes/planjumble.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PLANJUMBLE_H
+#define PLANJUMBLE_H
+
+#include "nodes/plannodes.h"
+#include "nodes/queryjumble.h"
+
+/* Values for the compute_plan_id GUC */
+enum ComputePlanIdType
+{
+	COMPUTE_PLAN_ID_OFF,
+	COMPUTE_PLAN_ID_ON,
+	COMPUTE_PLAN_ID_REGRESS,
+};
+
+/* GUC parameters */
+extern PGDLLIMPORT int compute_plan_id;
+
+extern void JumblePlanNode(JumbleState *jumble, List* rtable, Plan *plan);
+
+#endif							/* PLANJUMBLE_H */
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 4633121689..2f06d1d496 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -53,6 +53,8 @@ typedef struct PlannedStmt
 
 	uint64		queryId;		/* query identifier (copied from Query) */
 
+	uint64		planId;			/* plan identifier (calculated if compute_plan_id is enabled, can also be set by plugins) */
+
 	bool		hasReturning;	/* is it insert|update|delete|merge RETURNING? */
 
 	bool		hasModifyingCTE;	/* has insert|update|delete|merge in WITH? */
@@ -813,7 +815,7 @@ typedef struct NestLoop
 
 typedef struct NestLoopParam
 {
-	pg_node_attr(no_equal, no_query_jumble)
+	pg_node_attr(no_equal)
 
 	NodeTag		type;
 	int			paramno;		/* number of the PARAM_EXEC Param to set */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index b0ef1952e8..eac6511f81 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -1058,8 +1058,6 @@ typedef struct SubLink
  */
 typedef struct SubPlan
 {
-	pg_node_attr(no_query_jumble)
-
 	Expr		xpr;
 	/* Fields copied from original SubLink: */
 	SubLinkType subLinkType;	/* see above */
@@ -1090,8 +1088,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/utils/backend_status.h b/src/include/utils/backend_status.h
index 4e8b39a66d..8bb01256dd 100644
--- a/src/include/utils/backend_status.h
+++ b/src/include/utils/backend_status.h
@@ -170,6 +170,9 @@ typedef struct PgBackendStatus
 
 	/* query identifier, optionally computed using post_parse_analyze_hook */
 	uint64		st_query_id;
+
+	/* plan identifier, optionally computed after planning */
+	uint64		st_plan_id;
 } PgBackendStatus;
 
 
@@ -316,6 +319,7 @@ extern void pgstat_clear_backend_activity_snapshot(void);
 /* Activity reporting functions */
 extern void pgstat_report_activity(BackendState state, const char *cmd_str);
 extern void pgstat_report_query_id(uint64 query_id, bool force);
+extern void pgstat_report_plan_id(uint64 query_id, uint64 plan_id, bool force);
 extern void pgstat_report_tempfile(size_t filesize);
 extern void pgstat_report_appname(const char *appname);
 extern void pgstat_report_xact_timestamp(TimestampTz tstamp);
@@ -323,6 +327,7 @@ extern const char *pgstat_get_backend_current_activity(int pid, bool checkUser);
 extern const char *pgstat_get_crashed_backend_activity(int pid, char *buffer,
 													   int buflen);
 extern uint64 pgstat_get_my_query_id(void);
+extern uint64 pgstat_get_my_plan_id(void);
 extern BackendType pgstat_get_backend_type_by_proc_number(ProcNumber procNumber);
 
 
diff --git a/src/test/regress/expected/explain.out b/src/test/regress/expected/explain.out
index ee31e41d50..8bfa3c1a5f 100644
--- a/src/test/regress/expected/explain.out
+++ b/src/test/regress/expected/explain.out
@@ -699,6 +699,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 3014d047fe..c041a49dea 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1760,9 +1760,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,
@@ -1886,7 +1887,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,
@@ -2090,7 +2091,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,
@@ -2124,7 +2125,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 0bafa87049..d787ad2cda 100644
--- a/src/test/regress/sql/explain.sql
+++ b/src/test/regress/sql/explain.sql
@@ -167,6 +167,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');
-- 
2.47.1

