From be688ab9e3625c1b3ea855d20fbe0b4f68d9b43d Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Thu, 23 Jan 2025 13:06:42 -0600
Subject: [PATCH 1/1] Experiment with plan identifier computation

---
 .../pg_stat_statements/pg_stat_statements.c   |   2 +-
 src/backend/access/brin/brin.c                |   5 +
 src/backend/access/nbtree/nbtsort.c           |   7 +
 src/backend/catalog/system_views.sql          |   1 +
 src/backend/commands/createas.c               |   2 +-
 src/backend/commands/explain.c                |  16 ++
 src/backend/commands/extension.c              |   2 +-
 src/backend/commands/portalcmds.c             |   2 +-
 src/backend/commands/vacuumparallel.c         |   2 +
 src/backend/executor/execMain.c               |   2 +-
 src/backend/executor/execParallel.c           |   1 +
 src/backend/nodes/Makefile                    |   4 +-
 src/backend/nodes/gen_node_support.pl         |  95 ++++++++
 src/backend/nodes/planjumblefuncs.c           | 220 ++++++++++++++++++
 src/backend/nodes/queryjumblefuncs.c          |   2 +-
 src/backend/optimizer/plan/planner.c          |   5 +
 src/backend/parser/analyze.c                  |   2 +-
 src/backend/postmaster/launch_backend.c       |   5 +-
 src/backend/tcop/postgres.c                   |   2 +
 src/backend/utils/activity/backend_status.c   |  63 +++++
 src/backend/utils/adt/pgstatfuncs.c           |   8 +-
 src/backend/utils/error/csvlog.c              |   3 +
 src/backend/utils/error/elog.c                |   8 +
 src/backend/utils/error/jsonlog.c             |   3 +
 src/backend/utils/misc/guc_tables.c           |  30 ++-
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/catalog/pg_proc.dat               |   6 +-
 src/include/nodes/bitmapset.h                 |   2 +-
 src/include/nodes/{queryjumble.h => jumble.h} |  23 +-
 src/include/nodes/meson.build                 |   1 +
 src/include/nodes/parsenodes.h                |   4 +-
 src/include/nodes/plannodes.h                 |  54 ++---
 src/include/nodes/primnodes.h                 |   8 +-
 src/include/parser/analyze.h                  |   2 +-
 src/include/utils/backend_status.h            |   5 +
 src/test/regress/expected/rules.out           |   9 +-
 36 files changed, 553 insertions(+), 54 deletions(-)
 create mode 100644 src/backend/nodes/planjumblefuncs.c
 rename src/include/nodes/{queryjumble.h => jumble.h} (80%)

diff --git a/contrib/pg_stat_statements/pg_stat_statements.c b/contrib/pg_stat_statements/pg_stat_statements.c
index bebf8134eb..26ef7f3e03 100644
--- a/contrib/pg_stat_statements/pg_stat_statements.c
+++ b/contrib/pg_stat_statements/pg_stat_statements.c
@@ -55,7 +55,7 @@
 #include "jit/jit.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
-#include "nodes/queryjumble.h"
+#include "nodes/jumble.h"
 #include "optimizer/planner.h"
 #include "parser/analyze.h"
 #include "parser/scanner.h"
diff --git a/src/backend/access/brin/brin.c b/src/backend/access/brin/brin.c
index 4289142e20..33137862a1 100644
--- a/src/backend/access/brin/brin.c
+++ b/src/backend/access/brin/brin.c
@@ -70,6 +70,9 @@ typedef struct BrinShared
 	/* Query ID, for report in worker processes */
 	uint64		queryid;
 
+	/* Plan ID, for report in worker processes */
+	uint64		planid;
+
 	/*
 	 * workersdonecv is used to monitor the progress of workers.  All parallel
 	 * participants must indicate that they are done before leader can use
@@ -2452,6 +2455,7 @@ _brin_begin_parallel(BrinBuildState *buildstate, Relation heap, Relation index,
 	brinshared->scantuplesortstates = scantuplesortstates;
 	brinshared->pagesPerRange = buildstate->bs_pagesPerRange;
 	brinshared->queryid = pgstat_get_my_query_id();
+	brinshared->planid = pgstat_get_my_plan_id();
 	ConditionVariableInit(&brinshared->workersdonecv);
 	SpinLockInit(&brinshared->mutex);
 
@@ -2897,6 +2901,7 @@ _brin_parallel_build_main(dsm_segment *seg, shm_toc *toc)
 
 	/* Track query ID */
 	pgstat_report_query_id(brinshared->queryid, false);
+	pgstat_report_plan_id(brinshared->planid, false);
 
 	/* Open relations within worker */
 	heapRel = table_open(brinshared->heaprelid, heapLockmode);
diff --git a/src/backend/access/nbtree/nbtsort.c b/src/backend/access/nbtree/nbtsort.c
index 7aba852db9..7d90b2a94f 100644
--- a/src/backend/access/nbtree/nbtsort.c
+++ b/src/backend/access/nbtree/nbtsort.c
@@ -107,6 +107,9 @@ typedef struct BTShared
 	/* Query ID, for report in worker processes */
 	uint64		queryid;
 
+	/* Plan ID, for report in worker processes */
+	uint64		planid;
+
 	/*
 	 * workersdonecv is used to monitor the progress of workers.  All parallel
 	 * participants must indicate that they are done before leader can use
@@ -1508,6 +1511,7 @@ _bt_begin_parallel(BTBuildState *buildstate, bool isconcurrent, int request)
 	btshared->isconcurrent = isconcurrent;
 	btshared->scantuplesortstates = scantuplesortstates;
 	btshared->queryid = pgstat_get_my_query_id();
+	btshared->planid = pgstat_get_my_plan_id();
 	ConditionVariableInit(&btshared->workersdonecv);
 	SpinLockInit(&btshared->mutex);
 	/* Initialize mutable state */
@@ -1793,6 +1797,9 @@ _bt_parallel_build_main(dsm_segment *seg, shm_toc *toc)
 	/* Track query ID */
 	pgstat_report_query_id(btshared->queryid, false);
 
+	/* Track plan ID */
+	pgstat_report_plan_id(btshared->planid, false);
+
 	/* Open relations within worker */
 	heapRel = table_open(btshared->heaprelid, heapLockmode);
 	indexRel = index_open(btshared->indexrelid, indexLockmode);
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 46868bf7e8..a49efc6332 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -885,6 +885,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/createas.c b/src/backend/commands/createas.c
index 23cecd99c9..a8498e370c 100644
--- a/src/backend/commands/createas.c
+++ b/src/backend/commands/createas.c
@@ -37,7 +37,7 @@
 #include "commands/view.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
-#include "nodes/queryjumble.h"
+#include "nodes/jumble.h"
 #include "parser/analyze.h"
 #include "rewrite/rewriteHandler.h"
 #include "tcop/tcopprot.h"
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index c24e66f82e..d722b7048c 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -966,6 +966,22 @@ ExplainPrintPlan(ExplainState *es, QueryDesc *queryDesc)
 		ExplainPropertyInteger("Query Identifier", NULL, (int64)
 							   queryDesc->plannedstmt->queryId, es);
 	}
+
+	/*
+	 * COMPUTE_PLAN_ID_REGRESS means COMPUTE_PLAN_ID_AUTO, but we don't show
+	 * the planid 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_plans.planid column.
+		 */
+		ExplainPropertyInteger("Plan Identifier", NULL, (int64)
+							   queryDesc->plannedstmt->planId, es);
+	}
 }
 
 /*
diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index ba540e3de5..3a462d708b 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -54,7 +54,7 @@
 #include "funcapi.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
-#include "nodes/queryjumble.h"
+#include "nodes/jumble.h"
 #include "storage/fd.h"
 #include "tcop/utility.h"
 #include "utils/acl.h"
diff --git a/src/backend/commands/portalcmds.c b/src/backend/commands/portalcmds.c
index e7c8171c10..926ec2af36 100644
--- a/src/backend/commands/portalcmds.c
+++ b/src/backend/commands/portalcmds.c
@@ -28,7 +28,7 @@
 #include "executor/executor.h"
 #include "executor/tstoreReceiver.h"
 #include "miscadmin.h"
-#include "nodes/queryjumble.h"
+#include "nodes/jumble.h"
 #include "parser/analyze.h"
 #include "rewrite/rewriteHandler.h"
 #include "tcop/pquery.h"
diff --git a/src/backend/commands/vacuumparallel.c b/src/backend/commands/vacuumparallel.c
index 0d92e694d6..0de51d1a4a 100644
--- a/src/backend/commands/vacuumparallel.c
+++ b/src/backend/commands/vacuumparallel.c
@@ -64,6 +64,7 @@ typedef struct PVShared
 	Oid			relid;
 	int			elevel;
 	uint64		queryid;
+	uint64		planid;
 
 	/*
 	 * Fields for both index vacuum and cleanup.
@@ -371,6 +372,7 @@ parallel_vacuum_init(Relation rel, Relation *indrels, int nindexes,
 	shared->relid = RelationGetRelid(rel);
 	shared->elevel = elevel;
 	shared->queryid = pgstat_get_my_query_id();
+	shared->planid = pgstat_get_my_plan_id();
 	shared->maintenance_work_mem_worker =
 		(nindexes_mwm > 0) ?
 		maintenance_work_mem / Min(parallel_workers, nindexes_mwm) :
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index fb8dba3ab2..9ddfa5d867 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -50,7 +50,7 @@
 #include "foreign/fdwapi.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
-#include "nodes/queryjumble.h"
+#include "nodes/jumble.h"
 #include "parser/parse_relation.h"
 #include "pgstat.h"
 #include "rewrite/rewriteHandler.h"
diff --git a/src/backend/executor/execParallel.c b/src/backend/executor/execParallel.c
index ff4d9dd1bb..20ebc23b00 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..55902bee29 100644
--- a/src/backend/nodes/Makefile
+++ b/src/backend/nodes/Makefile
@@ -27,6 +27,7 @@ OBJS = \
 	params.o \
 	print.o \
 	queryjumblefuncs.o \
+	planjumblefuncs.o \
 	read.o \
 	readfuncs.o \
 	tidbitmap.o \
@@ -91,7 +92,8 @@ copyfuncs.o: copyfuncs.c copyfuncs.funcs.c copyfuncs.switch.c | node-support-sta
 equalfuncs.o: equalfuncs.c equalfuncs.funcs.c equalfuncs.switch.c | node-support-stamp
 outfuncs.o: outfuncs.c outfuncs.funcs.c outfuncs.switch.c | node-support-stamp
 queryjumblefuncs.o: queryjumblefuncs.c queryjumblefuncs.funcs.c queryjumblefuncs.switch.c | node-support-stamp
+planjumblefuncs.o: planjumblefuncs.c planjumblefuncs.funcs.c planjumblefuncs.switch.c | node-support-stamp
 readfuncs.o:  readfuncs.c readfuncs.funcs.c readfuncs.switch.c | node-support-stamp
 
 clean:
-	rm -f node-support-stamp $(addsuffix funcs.funcs.c,copy equal out queryjumble read) $(addsuffix funcs.switch.c,copy equal out queryjumble read) nodetags.h
+	rm -f node-support-stamp $(addsuffix funcs.funcs.c,copy equal out queryjumble planjumble read) $(addsuffix funcs.switch.c,copy equal out queryjumble read) nodetags.h
diff --git a/src/backend/nodes/gen_node_support.pl b/src/backend/nodes/gen_node_support.pl
index 7c012c27f8..77acb24e8b 100644
--- a/src/backend/nodes/gen_node_support.pl
+++ b/src/backend/nodes/gen_node_support.pl
@@ -131,6 +131,8 @@ my @no_read_write;
 my @special_read_write;
 # node types we don't want any support functions for, just node tags
 my @nodetag_only;
+# node types we don't want plan jumble support for
+my @no_plan_jumble;
 
 # types that are copied by straight assignment
 my @scalar_types = qw(
@@ -164,6 +166,7 @@ push @node_types, qw(List);
 push @no_copy, qw(List);
 push @no_equal, qw(List);
 push @no_query_jumble, qw(List);
+push @no_plan_jumble, qw(List);
 push @special_read_write, qw(List);
 
 # Nodes with custom copy/equal implementations are skipped from
@@ -176,6 +179,9 @@ my @custom_read_write;
 # Similarly for custom query jumble implementation.
 my @custom_query_jumble;
 
+# Similarly for custom plan jumble implementation.
+my @custom_plan_jumble;
+
 # Track node types with manually assigned NodeTag numbers.
 my %manual_nodetag_number;
 
@@ -346,6 +352,10 @@ foreach my $infile (@ARGV)
 						{
 							push @no_query_jumble, $in_struct;
 						}
+						elsif ($attr eq 'no_plan_jumble')
+						{
+							push @no_plan_jumble, $in_struct;
+						}
 						elsif ($attr eq 'no_read')
 						{
 							push @no_read, $in_struct;
@@ -424,6 +434,8 @@ foreach my $infile (@ARGV)
 						  if elem $supertype, @no_read;
 						push @no_query_jumble, $in_struct
 						  if elem $supertype, @no_query_jumble;
+						push @no_plan_jumble, $in_struct
+						  if elem $supertype, @no_plan_jumble;
 					}
 				}
 
@@ -475,6 +487,7 @@ foreach my $infile (@ARGV)
 								equal_ignore_if_zero
 								query_jumble_ignore
 								query_jumble_location
+								plan_jumble_ignore
 								read_write_ignore
 								write_only_relids
 								write_only_nondefault_pathtarget
@@ -1339,6 +1352,88 @@ _jumble${n}(JumbleState *jstate, Node *node)
 close $jff;
 close $jfs;
 
+# planjumblefuncs.c
+
+push @output_files, 'planjumblefuncs.funcs.c';
+open my $pjff, '>', "$output_path/planjumblefuncs.funcs.c$tmpext" or die $!;
+push @output_files, 'planjumblefuncs.switch.c';
+open my $pjfs, '>', "$output_path/planjumblefuncs.switch.c$tmpext" or die $!;
+
+printf $pjff $header_comment, 'planjumblefuncs.funcs.c';
+printf $pjfs $header_comment, 'planjumblefuncs.switch.c';
+
+print $pjff $node_includes;
+
+foreach my $n (@node_types)
+{
+	next if elem $n, @abstract_types;
+	next if elem $n, @nodetag_only;
+	my $struct_no_plan_jumble = (elem $n, @no_plan_jumble);
+
+	print $pjfs "\t\t\tcase T_${n}:\n"
+	  . "\t\t\t\t_jumble${n}(jstate, expr);\n"
+	  . "\t\t\t\tbreak;\n"
+	  unless $struct_no_plan_jumble;
+
+	next if elem $n, @custom_plan_jumble;
+
+	print $pjff "
+static void
+_jumble${n}(JumbleState *jstate, Node *node)
+{
+\t${n} *expr = (${n} *) node;\n
+" unless $struct_no_plan_jumble;
+
+	# print instructions for each field
+	foreach my $f (@{ $node_type_info{$n}->{fields} })
+	{
+		my $t = $node_type_info{$n}->{field_types}{$f};
+		my @a = @{ $node_type_info{$n}->{field_attrs}{$f} };
+		my $plan_jumble_ignore = $struct_no_plan_jumble;
+
+		# extract per-field attributes
+		foreach my $a (@a)
+		{
+			if ($a eq 'plan_jumble_ignore')
+			{
+				$plan_jumble_ignore = 1;
+			}
+		}
+
+		# node type
+		if (($t =~ /^(\w+)\*$/ or $t =~ /^struct\s+(\w+)\*$/)
+			and elem $1, @node_types)
+		{
+			print $pjff "\tJUMBLE_NODE($f);\n"
+			  unless $plan_jumble_ignore;
+		}
+		elsif ($t eq 'char*')
+		{
+			print $pjff "\tJUMBLE_STRING($f);\n"
+			  unless $plan_jumble_ignore;
+		}
+		else
+		{
+			print $pjff "\tJUMBLE_FIELD($f);\n"
+			  unless $plan_jumble_ignore;
+		}
+	}
+
+	# Some nodes have no attributes like CheckPointStmt,
+	# so tweak things for empty contents.
+	if (scalar(@{ $node_type_info{$n}->{fields} }) == 0)
+	{
+		print $pjff "\t(void) expr;\n"
+		  unless $struct_no_plan_jumble;
+	}
+
+	print $pjff "}
+" unless $struct_no_plan_jumble;
+}
+
+close $pjff;
+close $pjfs;
+
 # now rename the temporary files to their final names
 foreach my $file (@output_files)
 {
diff --git a/src/backend/nodes/planjumblefuncs.c b/src/backend/nodes/planjumblefuncs.c
new file mode 100644
index 0000000000..6080382ce7
--- /dev/null
+++ b/src/backend/nodes/planjumblefuncs.c
@@ -0,0 +1,220 @@
+/*-------------------------------------------------------------------------
+ *
+ * planjumblefuncs.c
+ *	 Plan fingerprinting.
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/nodes/planjumblefuncs.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "common/hashfn.h"
+#include "miscadmin.h"
+#include "nodes/jumble.h"
+#include "parser/scansup.h"
+#include "utils/backend_status.h"
+
+#define JUMBLE_SIZE				1024	/* query serialization buffer size */
+
+/* GUC parameters */
+int			compute_plan_id = COMPUTE_PLAN_ID_AUTO;
+
+/*
+ * 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 plan identifiers are computed in the core or not.
+ */
+bool		plan_id_enabled = false;
+
+static void AppendJumble(JumbleState *jstate,
+						 const unsigned char *item, Size size);
+static void _jumbleNode(JumbleState *jstate, Node *node);
+static void _jumbleList(JumbleState *jstate, Node *node);
+
+JumbleState *
+JumblePlan(PlannedStmt *ps)
+{
+	JumbleState *jstate = NULL;
+
+	Assert(IsPlanIdEnabled());
+
+	jstate = (JumbleState *) palloc(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;
+
+	/* Compute query ID and mark the Query node with it */
+	_jumbleNode(jstate, (Node *) ps->planTree);
+	ps->planId = DatumGetUInt64(hash_any_extended(jstate->jumble,
+												  jstate->jumble_len,
+												  0));
+	pgstat_report_plan_id(ps->planId, false);
+
+	return jstate;
+}
+
+/*
+ * Enables plan identifier computation.
+ *
+ * Third-party plugins can use this function to inform core that they require
+ * a plan identifier to be computed.
+ */
+void
+EnablePlanId(void)
+{
+	if (compute_plan_id != COMPUTE_PLAN_ID_OFF)
+		plan_id_enabled = true;
+}
+
+/*
+ * AppendJumble: Append a value that is substantive in a given query to
+ * the current jumble.
+ */
+static void
+AppendJumble(JumbleState *jstate, const unsigned char *item, Size size)
+{
+	unsigned char *jumble = jstate->jumble;
+	Size		jumble_len = jstate->jumble_len;
+
+	/*
+	 * Whenever the jumble buffer is full, we hash the current contents and
+	 * reset the buffer to contain just that hash value, thus relying on the
+	 * hash to summarize everything so far.
+	 */
+	while (size > 0)
+	{
+		Size		part_size;
+
+		if (jumble_len >= JUMBLE_SIZE)
+		{
+			uint64		start_hash;
+
+			start_hash = DatumGetUInt64(hash_any_extended(jumble,
+														  JUMBLE_SIZE, 0));
+			memcpy(jumble, &start_hash, sizeof(start_hash));
+			jumble_len = sizeof(start_hash);
+		}
+		part_size = Min(size, JUMBLE_SIZE - jumble_len);
+		memcpy(jumble + jumble_len, item, part_size);
+		jumble_len += part_size;
+		item += part_size;
+		size -= part_size;
+	}
+	jstate->jumble_len = jumble_len;
+}
+
+#define JUMBLE_NODE(item) \
+	_jumbleNode(jstate, (Node *) expr->item)
+#define JUMBLE_LOCATION(location) \
+	RecordConstLocation(jstate, expr->location)
+#define JUMBLE_FIELD(item) \
+	AppendJumble(jstate, (const unsigned char *) &(expr->item), sizeof(expr->item))
+#define JUMBLE_FIELD_SINGLE(item) \
+	AppendJumble(jstate, (const unsigned char *) &(item), sizeof(item))
+#define JUMBLE_STRING(str) \
+do { \
+	if (expr->str) \
+		AppendJumble(jstate, (const unsigned char *) (expr->str), strlen(expr->str) + 1); \
+} while(0)
+
+#include "planjumblefuncs.funcs.c"
+
+static void
+_jumbleNode(JumbleState *jstate, Node *node)
+{
+	Node	   *expr = node;
+
+	if (expr == NULL)
+		return;
+
+	/* Guard against stack overflow due to overly complex expressions */
+	check_stack_depth();
+
+	/*
+	 * We always emit the node's NodeTag, then any additional fields that are
+	 * considered significant, and then we recurse to any child nodes.
+	 */
+	JUMBLE_FIELD(type);
+
+	switch (nodeTag(expr))
+	{
+#include "planjumblefuncs.switch.c"
+
+		case T_List:
+		case T_IntList:
+		case T_OidList:
+		case T_XidList:
+			_jumbleList(jstate, expr);
+			break;
+		default:
+			/* Only a warning, since we can stumble along anyway */
+			elog(WARNING, "unrecognized node type: %d",
+				 (int) nodeTag(expr));
+			break;
+	}
+
+	/* Special cases to handle outside the automated code */
+	switch (nodeTag(expr))
+	{
+		case T_Param:
+			{
+				Param	   *p = (Param *) node;
+
+				/*
+				 * Update the highest Param id seen, in order to start
+				 * normalization correctly.
+				 */
+				if (p->paramkind == PARAM_EXTERN &&
+					p->paramid > jstate->highest_extern_param_id)
+					jstate->highest_extern_param_id = p->paramid;
+			}
+			break;
+		default:
+			break;
+	}
+}
+
+static void
+_jumbleList(JumbleState *jstate, Node *node)
+{
+	List	   *expr = (List *) node;
+	ListCell   *l;
+
+	switch (expr->type)
+	{
+		case T_List:
+			foreach(l, expr)
+				_jumbleNode(jstate, lfirst(l));
+			break;
+		case T_IntList:
+			foreach(l, expr)
+				JUMBLE_FIELD_SINGLE(lfirst_int(l));
+			break;
+		case T_OidList:
+			foreach(l, expr)
+				JUMBLE_FIELD_SINGLE(lfirst_oid(l));
+			break;
+		case T_XidList:
+			foreach(l, expr)
+				JUMBLE_FIELD_SINGLE(lfirst_xid(l));
+			break;
+		default:
+			elog(ERROR, "unrecognized list node type: %d",
+				 (int) expr->type);
+			return;
+	}
+}
diff --git a/src/backend/nodes/queryjumblefuncs.c b/src/backend/nodes/queryjumblefuncs.c
index b103a28193..9a3fc0478d 100644
--- a/src/backend/nodes/queryjumblefuncs.c
+++ b/src/backend/nodes/queryjumblefuncs.c
@@ -34,7 +34,7 @@
 
 #include "common/hashfn.h"
 #include "miscadmin.h"
-#include "nodes/queryjumble.h"
+#include "nodes/jumble.h"
 #include "parser/scansup.h"
 
 #define JUMBLE_SIZE				1024	/* query serialization buffer size */
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 6803edd085..e0df15fe0d 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/jumble.h"
 #include "nodes/supportnodes.h"
 #include "optimizer/appendinfo.h"
 #include "optimizer/clauses.h"
@@ -569,6 +570,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	result->utilityStmt = parse->utilityStmt;
 	result->stmt_location = parse->stmt_location;
 	result->stmt_len = parse->stmt_len;
+	result->planId = UINT64CONST(0);
 
 	result->jitFlags = PGJIT_NONE;
 	if (jit_enabled && jit_above_cost >= 0 &&
@@ -598,6 +600,9 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
 	if (glob->partition_directory != NULL)
 		DestroyPartitionDirectory(glob->partition_directory);
 
+	if (IsPlanIdEnabled())
+		JumblePlan(result);
+
 	return result;
 }
 
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 76f58b3aca..cd235921b4 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -31,7 +31,7 @@
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
-#include "nodes/queryjumble.h"
+#include "nodes/jumble.h"
 #include "optimizer/optimizer.h"
 #include "parser/analyze.h"
 #include "parser/parse_agg.h"
diff --git a/src/backend/postmaster/launch_backend.c b/src/backend/postmaster/launch_backend.c
index a97a1eda6d..3d34784c04 100644
--- a/src/backend/postmaster/launch_backend.c
+++ b/src/backend/postmaster/launch_backend.c
@@ -53,7 +53,7 @@
 #include "utils/memutils.h"
 
 #ifdef EXEC_BACKEND
-#include "nodes/queryjumble.h"
+#include "nodes/jumble.h"
 #include "storage/pg_shmem.h"
 #include "storage/spin.h"
 #endif
@@ -115,6 +115,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;
@@ -744,6 +745,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;
@@ -1004,6 +1006,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/tcop/postgres.c b/src/backend/tcop/postgres.c
index 5655348a2e..587f164b55 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, true);
 
 		/*
 		 * Get the command name for use in status display (it also becomes the
@@ -2163,6 +2164,7 @@ exec_execute_message(const char *portal_name, long max_rows)
 		if (stmt->queryId != UINT64CONST(0))
 		{
 			pgstat_report_query_id(stmt->queryId, false);
+			pgstat_report_plan_id(stmt->planId, false);
 			break;
 		}
 	}
diff --git a/src/backend/utils/activity/backend_status.c b/src/backend/utils/activity/backend_status.c
index 731342799a..7ccb2c6c6c 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);
 		}
@@ -593,7 +595,10 @@ pgstat_report_activity(BackendState state, const char *cmd_str)
 	 * identifier.
 	 */
 	if (state == STATE_RUNNING)
+	{
 		beentry->st_query_id = UINT64CONST(0);
+		beentry->st_plan_id = UINT64CONST(0);
+	}
 
 	if (cmd_str != NULL)
 	{
@@ -644,6 +649,44 @@ 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, 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 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 && !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 +1083,26 @@ pgstat_get_my_query_id(void)
 	return MyBEEntry->st_query_id;
 }
 
+/* ----------
+ * pgstat_get_my_plan_id() -
+ *
+ * Return current backend's query 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 0f5e0a9778..c033dd0778 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,11 @@ 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 +647,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/error/csvlog.c b/src/backend/utils/error/csvlog.c
index 8e9fbcb999..86650b02ff 100644
--- a/src/backend/utils/error/csvlog.c
+++ b/src/backend/utils/error/csvlog.c
@@ -250,6 +250,9 @@ write_csvlog(ErrorData *edata)
 	/* query id */
 	appendStringInfo(&buf, "%lld", (long long) pgstat_get_my_query_id());
 
+	/* plan id */
+	appendStringInfo(&buf, "%lld", (long long) pgstat_get_my_plan_id());
+
 	appendStringInfoChar(&buf, '\n');
 
 	/* If in the syslogger process, try to write messages direct to file */
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index 860bbd40d4..c8861b5090 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -3154,6 +3154,14 @@ log_status_format(StringInfo buf, const char *format, ErrorData *edata)
 					appendStringInfo(buf, "%lld",
 									 (long long) pgstat_get_my_query_id());
 				break;
+			case 'X':
+				if (padding != 0)
+					appendStringInfo(buf, "%*lld", padding,
+									 (long long) pgstat_get_my_plan_id());
+				else
+					appendStringInfo(buf, "%lld",
+									 (long long) pgstat_get_my_plan_id());
+				break;
 			default:
 				/* format error - ignore it */
 				break;
diff --git a/src/backend/utils/error/jsonlog.c b/src/backend/utils/error/jsonlog.c
index 6533f1d688..a88bd4339e 100644
--- a/src/backend/utils/error/jsonlog.c
+++ b/src/backend/utils/error/jsonlog.c
@@ -287,6 +287,9 @@ write_jsonlog(ErrorData *edata)
 	appendJSONKeyValueFmt(&buf, "query_id", false, "%lld",
 						  (long long) pgstat_get_my_query_id());
 
+	appendJSONKeyValueFmt(&buf, "plan_id", false, "%lld",
+						  (long long) pgstat_get_my_plan_id());
+
 	/* Finish string */
 	appendStringInfoChar(&buf, '}');
 	appendStringInfoChar(&buf, '\n');
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 38cb9e970d..bc02e7c103 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -50,7 +50,7 @@
 #include "libpq/auth.h"
 #include "libpq/libpq.h"
 #include "libpq/scram.h"
-#include "nodes/queryjumble.h"
+#include "nodes/jumble.h"
 #include "optimizer/cost.h"
 #include "optimizer/geqo.h"
 #include "optimizer/optimizer.h"
@@ -309,6 +309,24 @@ static const struct config_enum_entry compute_query_id_options[] = {
 	{NULL, 0, false}
 };
 
+/*
+ * Although only "on", "off", and "auto" 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".
@@ -4873,6 +4891,16 @@ struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"compute_plan_id", PGC_SUSET, STATS_MONITORING,
+			gettext_noop("Enables in-core computation of the plan tree."),
+			NULL
+		},
+		&compute_plan_id,
+		COMPUTE_PLAN_ID_AUTO, 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 079efa1baa..0634ae90dd 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -641,6 +641,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 18560755d2..de341e2d02 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/bitmapset.h b/src/include/nodes/bitmapset.h
index 03faca9308..a8694d646a 100644
--- a/src/include/nodes/bitmapset.h
+++ b/src/include/nodes/bitmapset.h
@@ -48,7 +48,7 @@ typedef int32 signedbitmapword; /* must be the matching signed type */
 
 typedef struct Bitmapset
 {
-	pg_node_attr(custom_copy_equal, special_read_write, no_query_jumble)
+	pg_node_attr(custom_copy_equal, special_read_write, no_query_jumble, no_plan_jumble)
 
 	NodeTag		type;
 	int			nwords;			/* number of words in array */
diff --git a/src/include/nodes/queryjumble.h b/src/include/nodes/jumble.h
similarity index 80%
rename from src/include/nodes/queryjumble.h
rename to src/include/nodes/jumble.h
index 50eb956658..3cd35f145f 100644
--- a/src/include/nodes/queryjumble.h
+++ b/src/include/nodes/jumble.h
@@ -1,13 +1,13 @@
 /*-------------------------------------------------------------------------
  *
- * queryjumble.h
+ * jumble.h
  *	  Query normalization and fingerprinting.
  *
  * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
  *
  * IDENTIFICATION
- *	  src/include/nodes/queryjumble.h
+ *	  src/include/nodes/jumble.h
  *
  *-------------------------------------------------------------------------
  */
@@ -15,6 +15,7 @@
 #define QUERYJUMBLE_H
 
 #include "nodes/parsenodes.h"
+#include "nodes/plannodes.h"
 
 /*
  * Struct for tracking locations/lengths of constants during normalization
@@ -57,17 +58,25 @@ enum ComputeQueryIdType
 	COMPUTE_QUERY_ID_ON,
 	COMPUTE_QUERY_ID_AUTO,
 	COMPUTE_QUERY_ID_REGRESS,
+	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 JumbleState *JumblePlan(PlannedStmt *ps);
 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
@@ -83,4 +92,14 @@ IsQueryIdEnabled(void)
 	return query_id_enabled;
 }
 
+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;
+}
+
 #endif							/* QUERYJUMBLE_H */
diff --git a/src/include/nodes/meson.build b/src/include/nodes/meson.build
index f3dd5461fe..2e73b39640 100644
--- a/src/include/nodes/meson.build
+++ b/src/include/nodes/meson.build
@@ -37,6 +37,7 @@ node_support_output = [
   'copyfuncs.funcs.c', 'copyfuncs.switch.c',
   'equalfuncs.funcs.c', 'equalfuncs.switch.c',
   'queryjumblefuncs.funcs.c', 'queryjumblefuncs.switch.c',
+  'planjumblefuncs.funcs.c', 'planjumblefuncs.switch.c',
 ]
 node_support_install = [
   dir_include_server / 'nodes',
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index ffe155ee20..8bf2b11f8b 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -367,7 +367,7 @@ union ValUnion
 
 typedef struct A_Const
 {
-	pg_node_attr(custom_copy_equal, custom_read_write, custom_query_jumble)
+	pg_node_attr(custom_copy_equal, custom_read_write, custom_query_jumble, no_plan_jumble)
 
 	NodeTag		type;
 	union ValUnion val;
@@ -2670,7 +2670,7 @@ typedef enum VariableSetKind
 
 typedef struct VariableSetStmt
 {
-	pg_node_attr(custom_query_jumble)
+	pg_node_attr(custom_query_jumble, no_plan_jumble)
 
 	NodeTag		type;
 	VariableSetKind kind;
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 9e19cdd284..d3eb40e5ea 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 (copied from PlannedStmt) */
+
 	bool		hasReturning;	/* is it insert|update|delete|merge RETURNING? */
 
 	bool		hasModifyingCTE;	/* has insert|update|delete|merge in WITH? */
@@ -125,33 +127,33 @@ typedef struct Plan
 	/*
 	 * estimated execution costs for plan (see costsize.c for more info)
 	 */
-	int			disabled_nodes; /* count of disabled nodes */
-	Cost		startup_cost;	/* cost expended before fetching any tuples */
-	Cost		total_cost;		/* total cost (assuming all tuples fetched) */
+	int			disabled_nodes pg_node_attr(plan_jumble_ignore); /* count of disabled nodes */
+	Cost		startup_cost pg_node_attr(plan_jumble_ignore);	/* cost expended before fetching any tuples */
+	Cost		total_cost pg_node_attr(plan_jumble_ignore);		/* total cost (assuming all tuples fetched) */
 
 	/*
 	 * planner's estimate of result size of this plan step
 	 */
-	Cardinality plan_rows;		/* number of rows plan is expected to emit */
-	int			plan_width;		/* average row width in bytes */
+	Cardinality plan_rows pg_node_attr(plan_jumble_ignore);	/* number of rows plan is expected to emit */
+	int			plan_width pg_node_attr(plan_jumble_ignore);		/* average row width in bytes */
 
 	/*
 	 * information needed for parallel query
 	 */
-	bool		parallel_aware; /* engage parallel-aware logic? */
-	bool		parallel_safe;	/* OK to use as part of parallel plan? */
+	bool		parallel_aware pg_node_attr(plan_jumble_ignore); /* engage parallel-aware logic? */
+	bool		parallel_safe pg_node_attr(plan_jumble_ignore);	/* OK to use as part of parallel plan? */
 
 	/*
 	 * information needed for asynchronous execution
 	 */
-	bool		async_capable;	/* engage asynchronous-capable logic? */
+	bool		async_capable pg_node_attr(plan_jumble_ignore);	/* engage asynchronous-capable logic? */
 
 	/*
 	 * Common structural data for all Plan types.
 	 */
-	int			plan_node_id;	/* unique across entire final plan tree */
+	int			plan_node_id pg_node_attr(plan_jumble_ignore);	/* unique across entire final plan tree */
 	List	   *targetlist;		/* target list to be computed at this node */
-	List	   *qual;			/* implicitly-ANDed qual conditions */
+	List	   *qual pg_node_attr(plan_jumble_ignore);			/* implicitly-ANDed qual conditions */
 	struct Plan *lefttree;		/* input plan tree(s) */
 	struct Plan *righttree;
 	List	   *initPlan;		/* Init Plan nodes (un-correlated expr
@@ -168,8 +170,8 @@ typedef struct Plan
 	 * params that affect the node (i.e., the setParams of its initplans).
 	 * These are _all_ the PARAM_EXEC params that affect this node.
 	 */
-	Bitmapset  *extParam;
-	Bitmapset  *allParam;
+	Bitmapset  *extParam pg_node_attr(plan_jumble_ignore);
+	Bitmapset  *allParam pg_node_attr(plan_jumble_ignore);
 } Plan;
 
 /* ----------------
@@ -268,7 +270,7 @@ struct PartitionPruneInfo;		/* forward reference to struct below */
 typedef struct Append
 {
 	Plan		plan;
-	Bitmapset  *apprelids;		/* RTIs of appendrel(s) formed by this node */
+	Bitmapset  *apprelids pg_node_attr(plan_jumble_ignore);		/* RTIs of appendrel(s) formed by this node */
 	List	   *appendplans;
 	int			nasyncplans;	/* # of asynchronous plans */
 
@@ -292,7 +294,7 @@ typedef struct MergeAppend
 	Plan		plan;
 
 	/* RTIs of appendrel(s) formed by this node */
-	Bitmapset  *apprelids;
+	Bitmapset  *apprelids pg_node_attr(plan_jumble_ignore);
 
 	List	   *mergeplans;
 
@@ -389,7 +391,7 @@ typedef struct Scan
 	pg_node_attr(abstract)
 
 	Plan		plan;
-	Index		scanrelid;		/* relid is index into the range table */
+	Index		scanrelid pg_node_attr(plan_jumble_ignore);		/* relid is index into the range table */
 } Scan;
 
 /* ----------------
@@ -719,8 +721,8 @@ typedef struct ForeignScan
 	List	   *fdw_private;	/* private data for FDW */
 	List	   *fdw_scan_tlist; /* optional tlist describing scan tuple */
 	List	   *fdw_recheck_quals;	/* original quals not in scan.plan.qual */
-	Bitmapset  *fs_relids;		/* base+OJ RTIs generated by this scan */
-	Bitmapset  *fs_base_relids; /* base RTIs generated by this scan */
+	Bitmapset  *fs_relids pg_node_attr(plan_jumble_ignore);		/* base+OJ RTIs generated by this scan */
+	Bitmapset  *fs_base_relids pg_node_attr(plan_jumble_ignore); /* base RTIs generated by this scan */
 	bool		fsSystemCol;	/* true if any "system column" is needed */
 } ForeignScan;
 
@@ -748,7 +750,7 @@ typedef struct CustomScan
 	List	   *custom_exprs;	/* expressions that custom code may evaluate */
 	List	   *custom_private; /* private data for custom code */
 	List	   *custom_scan_tlist;	/* optional tlist describing scan tuple */
-	Bitmapset  *custom_relids;	/* RTIs generated by this scan */
+	Bitmapset  *custom_relids pg_node_attr(plan_jumble_ignore);	/* RTIs generated by this scan */
 
 	/*
 	 * NOTE: The method field of CustomScan is required to be a pointer to a
@@ -924,7 +926,7 @@ typedef struct Memoize
 	uint32		est_entries;
 
 	/* paramids from param_exprs */
-	Bitmapset  *keyparamids;
+	Bitmapset  *keyparamids pg_node_attr(plan_jumble_ignore);
 } Memoize;
 
 /* ----------------
@@ -1023,7 +1025,7 @@ typedef struct Agg
 	uint64		transitionSpace;
 
 	/* IDs of Params used in Aggref inputs */
-	Bitmapset  *aggParams;
+	Bitmapset  *aggParams pg_node_attr(plan_jumble_ignore);
 
 	/* Note: planner provides numGroups & aggParams only in HASHED/MIXED case */
 
@@ -1147,7 +1149,7 @@ typedef struct Gather
 	int			rescan_param;	/* ID of Param that signals a rescan, or -1 */
 	bool		single_copy;	/* don't execute plan more than once */
 	bool		invisible;		/* suppress EXPLAIN display (for testing)? */
-	Bitmapset  *initParam;		/* param id's of initplans which are referred
+	Bitmapset  *initParam pg_node_attr(plan_jumble_ignore);	/* param id's of initplans which are referred
 								 * at gather or one of it's child node */
 } Gather;
 
@@ -1186,7 +1188,7 @@ typedef struct GatherMerge
 	 * param id's of initplans which are referred at gather merge or one of
 	 * it's child node
 	 */
-	Bitmapset  *initParam;
+	Bitmapset  *initParam pg_node_attr(plan_jumble_ignore);
 } GatherMerge;
 
 /* ----------------
@@ -1426,7 +1428,7 @@ typedef struct PartitionPruneInfo
 
 	NodeTag		type;
 	List	   *prune_infos;
-	Bitmapset  *other_subplans;
+	Bitmapset  *other_subplans pg_node_attr(plan_jumble_ignore);
 } PartitionPruneInfo;
 
 /*
@@ -1456,7 +1458,7 @@ typedef struct PartitionedRelPruneInfo
 	Index		rtindex;
 
 	/* Indexes of all partitions which subplans or subparts are present for */
-	Bitmapset  *present_parts;
+	Bitmapset  *present_parts pg_node_attr(plan_jumble_ignore);
 
 	/* Length of the following arrays: */
 	int			nparts;
@@ -1480,7 +1482,7 @@ typedef struct PartitionedRelPruneInfo
 	List	   *exec_pruning_steps; /* List of PartitionPruneStep */
 
 	/* All PARAM_EXEC Param IDs in exec_pruning_steps */
-	Bitmapset  *execparamids;
+	Bitmapset  *execparamids pg_node_attr(plan_jumble_ignore);
 } PartitionedRelPruneInfo;
 
 /*
@@ -1531,7 +1533,7 @@ typedef struct PartitionPruneStepOp
 	StrategyNumber opstrategy;
 	List	   *exprs;
 	List	   *cmpfns;
-	Bitmapset  *nullkeys;
+	Bitmapset  *nullkeys pg_node_attr(plan_jumble_ignore);
 } PartitionPruneStepOp;
 
 /*
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 59e7bb26bb..8eb9959496 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -2246,15 +2246,15 @@ typedef struct TargetEntry
 	/* attribute number (see notes above) */
 	AttrNumber	resno;
 	/* name of the column (could be NULL) */
-	char	   *resname pg_node_attr(query_jumble_ignore);
+	char	   *resname pg_node_attr(query_jumble_ignore, plan_jumble_ignore);
 	/* nonzero if referenced by a sort/group clause */
 	Index		ressortgroupref;
 	/* OID of column's source table */
-	Oid			resorigtbl pg_node_attr(query_jumble_ignore);
+	Oid			resorigtbl pg_node_attr(query_jumble_ignore, plan_jumble_ignore);
 	/* column's number in source table */
-	AttrNumber	resorigcol pg_node_attr(query_jumble_ignore);
+	AttrNumber	resorigcol pg_node_attr(query_jumble_ignore, plan_jumble_ignore);
 	/* set to true to eliminate the attribute from final target list */
-	bool		resjunk pg_node_attr(query_jumble_ignore);
+	bool		resjunk pg_node_attr(query_jumble_ignore, plan_jumble_ignore);
 } TargetEntry;
 
 
diff --git a/src/include/parser/analyze.h b/src/include/parser/analyze.h
index f1bd18c49f..e414b630bc 100644
--- a/src/include/parser/analyze.h
+++ b/src/include/parser/analyze.h
@@ -15,7 +15,7 @@
 #define ANALYZE_H
 
 #include "nodes/params.h"
-#include "nodes/queryjumble.h"
+#include "nodes/jumble.h"
 #include "parser/parse_node.h"
 
 /* Hook for plugins to get control at end of parse analysis */
diff --git a/src/include/utils/backend_status.h b/src/include/utils/backend_status.h
index d3d4ff6c5c..e074bc5b45 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 using planner_hook */
+	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, 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/rules.out b/src/test/regress/expected/rules.out
index 856a8349c5..e20cc7d28c 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,
@@ -2092,7 +2093,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,
@@ -2126,7 +2127,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,
-- 
2.39.5 (Apple Git-154)

