From 394c75c8d49ebf7afb5ce838314ab2fe1ad01c29 Mon Sep 17 00:00:00 2001
From: Dean Rasheed <dean.a.rasheed@gmail.com>
Date: Thu, 18 Jun 2026 23:38:12 +0100
Subject: [PATCH v2 8/9] Add pg_temp_statistic_ext_data global temporary
 catalog table.

This has the same columns as pg_statistic_ext_data, but it is a global
temporary table, and is used to hold extended statistics data for
global temporary tables, allowing the data to be session-specific
while the extended statistics object definitions remain global.
---
 src/backend/catalog/system_views.sql          | 14 +++-
 src/backend/commands/statscmds.c              | 21 ++++--
 src/backend/optimizer/util/plancat.c          | 13 ++--
 src/backend/statistics/dependencies.c         | 11 +--
 src/backend/statistics/extended_stats.c       | 27 ++++---
 src/backend/statistics/extended_stats_funcs.c | 48 ++++++++++---
 src/backend/statistics/mcv.c                  | 13 ++--
 src/backend/statistics/mvdistinct.c           | 10 ++-
 src/backend/utils/adt/selfuncs.c              |  5 +-
 src/backend/utils/cache/relcache.c            |  1 +
 src/include/catalog/Makefile                  |  3 +-
 src/include/catalog/meson.build               |  1 +
 src/include/catalog/pg_statistic_ext_data.h   |  3 +
 .../catalog/pg_temp_statistic_ext_data.h      | 62 ++++++++++++++++
 src/include/commands/defrem.h                 |  2 +-
 src/include/statistics/statistics.h           |  8 +--
 src/test/regress/expected/global_temp.out     | 71 +++++++++++++++++++
 src/test/regress/expected/oidjoins.out        |  1 +
 src/test/regress/expected/rules.out           | 19 ++++-
 src/test/regress/expected/sanity_check.out    | 24 +++++++
 src/test/regress/sql/global_temp.sql          | 41 +++++++++++
 src/test/regress/sql/sanity_check.sql         | 21 ++++++
 22 files changed, 365 insertions(+), 54 deletions(-)
 create mode 100644 src/include/catalog/pg_temp_statistic_ext_data.h

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 8e6ff7c37a5..63b89faebee 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -284,6 +284,11 @@ REVOKE ALL ON pg_statistic FROM public;
 REVOKE ALL ON pg_temp_statistic FROM public;
 REVOKE ALL ON pg_all_statistic FROM public;
 
+CREATE VIEW pg_all_statistic_ext_data AS
+    SELECT * FROM pg_statistic_ext_data
+    UNION ALL
+    SELECT * FROM pg_temp_statistic_ext_data;
+
 CREATE VIEW pg_stats_ext WITH (security_barrier) AS
     SELECT cn.nspname AS schemaname,
            c.relname AS tablename,
@@ -307,7 +312,7 @@ CREATE VIEW pg_stats_ext WITH (security_barrier) AS
            m.most_common_freqs,
            m.most_common_base_freqs
     FROM pg_statistic_ext s JOIN pg_class c ON (c.oid = s.stxrelid)
-         JOIN pg_statistic_ext_data sd ON (s.oid = sd.stxoid)
+         JOIN pg_all_statistic_ext_data sd ON (s.oid = sd.stxoid)
          LEFT JOIN pg_namespace cn ON (cn.oid = c.relnamespace)
          LEFT JOIN pg_namespace sn ON (sn.oid = s.stxnamespace)
          LEFT JOIN LATERAL
@@ -404,7 +409,7 @@ CREATE VIEW pg_stats_ext_exprs WITH (security_barrier) AS
                WHEN (stat.a).stakind5 = 7 THEN (stat.a).stavalues5
                END) AS range_bounds_histogram
     FROM pg_statistic_ext s JOIN pg_class c ON (c.oid = s.stxrelid)
-         LEFT JOIN pg_statistic_ext_data sd ON (s.oid = sd.stxoid)
+         LEFT JOIN pg_all_statistic_ext_data sd ON (s.oid = sd.stxoid)
          LEFT JOIN pg_namespace cn ON (cn.oid = c.relnamespace)
          LEFT JOIN pg_namespace sn ON (sn.oid = s.stxnamespace)
          JOIN LATERAL (
@@ -414,8 +419,11 @@ CREATE VIEW pg_stats_ext_exprs WITH (security_barrier) AS
     WHERE pg_has_role(c.relowner, 'USAGE')
     AND (c.relrowsecurity = false OR NOT row_security_active(c.oid));
 
--- unprivileged users may read pg_statistic_ext but not pg_statistic_ext_data
+-- unprivileged users may read pg_statistic_ext but not pg_statistic_ext_data,
+-- pg_temp_statistic_ext_data, or pg_all_statistic_ext_data
 REVOKE ALL ON pg_statistic_ext_data FROM public;
+REVOKE ALL ON pg_temp_statistic_ext_data FROM public;
+REVOKE ALL ON pg_all_statistic_ext_data FROM public;
 
 CREATE VIEW pg_publication_tables AS
     SELECT
diff --git a/src/backend/commands/statscmds.c b/src/backend/commands/statscmds.c
index b354723be44..95ed3bd0b21 100644
--- a/src/backend/commands/statscmds.c
+++ b/src/backend/commands/statscmds.c
@@ -25,6 +25,7 @@
 #include "catalog/pg_namespace.h"
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_statistic_ext_data.h"
+#include "catalog/pg_temp_statistic_ext_data.h"
 #include "commands/comment.h"
 #include "commands/defrem.h"
 #include "miscadmin.h"
@@ -775,14 +776,24 @@ AlterStatistics(AlterStatsStmt *stmt)
  * exists, so don't error out.
  */
 void
-RemoveStatisticsDataById(Oid statsOid, bool inh)
+RemoveStatisticsDataById(Oid relid, Oid statsOid, bool inh)
 {
 	Relation	relation;
+	SysCacheIdentifier cacheId;
 	HeapTuple	tup;
 
-	relation = table_open(StatisticExtDataRelationId, RowExclusiveLock);
+	if (rel_is_global_temp(relid))
+	{
+		relation = table_open(TempStatisticExtDataRelationId, RowExclusiveLock);
+		cacheId = TEMPSTATEXTDATASTXOID;
+	}
+	else
+	{
+		relation = table_open(StatisticExtDataRelationId, RowExclusiveLock);
+		cacheId = STATEXTDATASTXOID;
+	}
 
-	tup = SearchSysCache2(STATEXTDATASTXOID, ObjectIdGetDatum(statsOid),
+	tup = SearchSysCache2(cacheId, ObjectIdGetDatum(statsOid),
 						  BoolGetDatum(inh));
 
 	/* We don't know if the data row for inh value exists. */
@@ -830,8 +841,8 @@ RemoveStatisticsById(Oid statsOid)
 	 */
 	rel = table_open(relid, ShareUpdateExclusiveLock);
 
-	RemoveStatisticsDataById(statsOid, true);
-	RemoveStatisticsDataById(statsOid, false);
+	RemoveStatisticsDataById(relid, statsOid, true);
+	RemoveStatisticsDataById(relid, statsOid, false);
 
 	CacheInvalidateRelcacheByRelid(relid);
 
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 7c4be174869..1c71b1ff969 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -1652,14 +1652,17 @@ get_relation_constraints(PlannerInfo *root,
  * The result is stored in stainfos list.
  */
 static void
-get_relation_statistics_worker(List **stainfos, RelOptInfo *rel,
+get_relation_statistics_worker(Oid relid, List **stainfos, RelOptInfo *rel,
 							   Oid statOid, bool inh,
 							   Bitmapset *keys, List *exprs)
 {
+	SysCacheIdentifier cacheId;
 	Form_pg_statistic_ext_data dataForm;
 	HeapTuple	dtup;
 
-	dtup = SearchSysCache2(STATEXTDATASTXOID,
+	cacheId = rel_is_global_temp(relid) ? TEMPSTATEXTDATASTXOID : STATEXTDATASTXOID;
+
+	dtup = SearchSysCache2(cacheId,
 						   ObjectIdGetDatum(statOid), BoolGetDatum(inh));
 	if (!HeapTupleIsValid(dtup))
 		return;
@@ -1824,9 +1827,11 @@ get_relation_statistics(PlannerInfo *root, RelOptInfo *rel,
 
 		/* extract statistics for possible values of stxdinherit flag */
 
-		get_relation_statistics_worker(&stainfos, rel, statOid, true, keys, exprs);
+		get_relation_statistics_worker(RelationGetRelid(relation), &stainfos,
+									   rel, statOid, true, keys, exprs);
 
-		get_relation_statistics_worker(&stainfos, rel, statOid, false, keys, exprs);
+		get_relation_statistics_worker(RelationGetRelid(relation), &stainfos,
+									   rel, statOid, false, keys, exprs);
 
 		ReleaseSysCache(htup);
 		bms_free(keys);
diff --git a/src/backend/statistics/dependencies.c b/src/backend/statistics/dependencies.c
index 95dcc218978..0d75b8d7840 100644
--- a/src/backend/statistics/dependencies.c
+++ b/src/backend/statistics/dependencies.c
@@ -685,20 +685,23 @@ dependency_is_fully_matched(MVDependency *dependency, Bitmapset *attnums)
  *		Load the functional dependencies for the indicated pg_statistic_ext tuple
  */
 MVDependencies *
-statext_dependencies_load(Oid mvoid, bool inh)
+statext_dependencies_load(Oid relid, Oid mvoid, bool inh)
 {
+	SysCacheIdentifier cacheId;
 	MVDependencies *result;
 	bool		isnull;
 	Datum		deps;
 	HeapTuple	htup;
 
-	htup = SearchSysCache2(STATEXTDATASTXOID,
+	cacheId = rel_is_global_temp(relid) ? TEMPSTATEXTDATASTXOID : STATEXTDATASTXOID;
+
+	htup = SearchSysCache2(cacheId,
 						   ObjectIdGetDatum(mvoid),
 						   BoolGetDatum(inh));
 	if (!HeapTupleIsValid(htup))
 		elog(ERROR, "cache lookup failed for statistics object %u", mvoid);
 
-	deps = SysCacheGetAttr(STATEXTDATASTXOID, htup,
+	deps = SysCacheGetAttr(cacheId, htup,
 						   Anum_pg_statistic_ext_data_stxddependencies, &isnull);
 	if (isnull)
 		elog(ERROR,
@@ -1608,7 +1611,7 @@ dependencies_clauselist_selectivity(PlannerInfo *root,
 		if (nmatched + nexprs < 2)
 			continue;
 
-		deps = statext_dependencies_load(stat->statOid, rte->inh);
+		deps = statext_dependencies_load(rte->relid, stat->statOid, rte->inh);
 
 		/*
 		 * The expressions may be represented by different attnums in the
diff --git a/src/backend/statistics/extended_stats.c b/src/backend/statistics/extended_stats.c
index 2b83355d26e..0c52bc083f6 100644
--- a/src/backend/statistics/extended_stats.c
+++ b/src/backend/statistics/extended_stats.c
@@ -23,6 +23,7 @@
 #include "catalog/indexing.h"
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_statistic_ext_data.h"
+#include "catalog/pg_temp_statistic_ext_data.h"
 #include "commands/defrem.h"
 #include "commands/progress.h"
 #include "executor/executor.h"
@@ -77,7 +78,7 @@ typedef struct StatExtEntry
 static List *fetch_statentries_for_relation(Relation pg_statext, Relation rel);
 static VacAttrStats **lookup_var_attr_stats(Bitmapset *attrs, List *exprs,
 											int nvacatts, VacAttrStats **vacatts);
-static void statext_store(Oid statOid, bool inh,
+static void statext_store(Oid relid, Oid statOid, bool inh,
 						  MVNDistinct *ndistinct, MVDependencies *dependencies,
 						  MCVList *mcv, Datum exprs, VacAttrStats **stats);
 static int	statext_compute_stattarget(int stattarget,
@@ -227,7 +228,7 @@ BuildRelationExtStatistics(Relation onerel, bool inh, double totalrows,
 		}
 
 		/* store the statistics in the catalog */
-		statext_store(stat->statOid, inh,
+		statext_store(RelationGetRelid(onerel), stat->statOid, inh,
 					  ndistinct, dependencies, mcv, exprstats, stats);
 
 		/* for reporting progress */
@@ -805,7 +806,7 @@ lookup_var_attr_stats(Bitmapset *attrs, List *exprs,
  *	tuple.
  */
 static void
-statext_store(Oid statOid, bool inh,
+statext_store(Oid relid, Oid statOid, bool inh,
 			  MVNDistinct *ndistinct, MVDependencies *dependencies,
 			  MCVList *mcv, Datum exprs, VacAttrStats **stats)
 {
@@ -814,7 +815,12 @@ statext_store(Oid statOid, bool inh,
 	Datum		values[Natts_pg_statistic_ext_data];
 	bool		nulls[Natts_pg_statistic_ext_data];
 
-	pg_stextdata = table_open(StatisticExtDataRelationId, RowExclusiveLock);
+	if (rel_is_global_temp(relid))
+		pg_stextdata = table_open(TempStatisticExtDataRelationId,
+								  RowExclusiveLock);
+	else
+		pg_stextdata = table_open(StatisticExtDataRelationId,
+								  RowExclusiveLock);
 
 	memset(nulls, true, sizeof(nulls));
 	memset(values, 0, sizeof(values));
@@ -862,7 +868,7 @@ statext_store(Oid statOid, bool inh,
 	 * Delete the old tuple if it exists, and insert a new one. It's easier
 	 * than trying to update or insert, based on various conditions.
 	 */
-	RemoveStatisticsDataById(statOid, inh);
+	RemoveStatisticsDataById(relid, statOid, inh);
 
 	/* form and insert a new tuple */
 	stup = heap_form_tuple(RelationGetDescr(pg_stextdata), values, nulls);
@@ -1891,7 +1897,7 @@ statext_mcv_clauselist_selectivity(PlannerInfo *root, List *clauses, int varReli
 			MCVList    *mcv_list;
 
 			/* Load the MCV list stored in the statistics object */
-			mcv_list = statext_mcv_load(stat->statOid, rte->inh);
+			mcv_list = statext_mcv_load(rte->relid, stat->statOid, rte->inh);
 
 			/*
 			 * Compute the selectivity of the ORed list of clauses covered by
@@ -2450,8 +2456,9 @@ serialize_expr_stats(AnlExprData *exprdata, int nexprs)
  * data to use.
  */
 HeapTuple
-statext_expressions_load(Oid stxoid, bool inh, int idx)
+statext_expressions_load(Oid relid, Oid stxoid, bool inh, int idx)
 {
+	SysCacheIdentifier cacheId;
 	bool		isnull;
 	Datum		value;
 	HeapTuple	htup;
@@ -2460,12 +2467,14 @@ statext_expressions_load(Oid stxoid, bool inh, int idx)
 	HeapTupleData tmptup;
 	HeapTuple	tup;
 
-	htup = SearchSysCache2(STATEXTDATASTXOID,
+	cacheId = rel_is_global_temp(relid) ? TEMPSTATEXTDATASTXOID : STATEXTDATASTXOID;
+
+	htup = SearchSysCache2(cacheId,
 						   ObjectIdGetDatum(stxoid), BoolGetDatum(inh));
 	if (!HeapTupleIsValid(htup))
 		elog(ERROR, "cache lookup failed for statistics object %u", stxoid);
 
-	value = SysCacheGetAttr(STATEXTDATASTXOID, htup,
+	value = SysCacheGetAttr(cacheId, htup,
 							Anum_pg_statistic_ext_data_stxdexpr, &isnull);
 	if (isnull)
 		elog(ERROR,
diff --git a/src/backend/statistics/extended_stats_funcs.c b/src/backend/statistics/extended_stats_funcs.c
index 2cb3942056f..545b3f191d3 100644
--- a/src/backend/statistics/extended_stats_funcs.c
+++ b/src/backend/statistics/extended_stats_funcs.c
@@ -24,6 +24,7 @@
 #include "catalog/pg_operator.h"
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_statistic_ext_data.h"
+#include "catalog/pg_temp_statistic_ext_data.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "nodes/nodeFuncs.h"
@@ -125,7 +126,7 @@ static bool extended_statistics_update(FunctionCallInfo fcinfo);
 
 static HeapTuple get_pg_statistic_ext(Relation pg_stext, Oid nspoid,
 									  const char *stxname);
-static bool delete_pg_statistic_ext_data(Oid stxoid, bool inherited);
+static bool delete_pg_statistic_ext_data(Oid relid, Oid stxoid, bool inherited);
 
 /*
  * Track the extended statistics kinds expected for a pg_statistic_ext
@@ -140,7 +141,8 @@ typedef struct
 } StakindFlags;
 
 static void expand_stxkind(HeapTuple tup, StakindFlags *enabled);
-static void upsert_pg_statistic_ext_data(const Datum *values,
+static void upsert_pg_statistic_ext_data(Oid relid,
+										 const Datum *values,
 										 const bool *nulls,
 										 const bool *replaces);
 
@@ -265,16 +267,28 @@ expand_stxkind(HeapTuple tup, StakindFlags *enabled)
  * Perform the actual storage of a pg_statistic_ext_data tuple.
  */
 static void
-upsert_pg_statistic_ext_data(const Datum *values, const bool *nulls,
-							 const bool *replaces)
+upsert_pg_statistic_ext_data(Oid relid, const Datum *values,
+							 const bool *nulls, const bool *replaces)
 {
 	Relation	pg_stextdata;
+	SysCacheIdentifier cacheId;
 	HeapTuple	stxdtup;
 	HeapTuple	newtup;
 
-	pg_stextdata = table_open(StatisticExtDataRelationId, RowExclusiveLock);
+	if (rel_is_global_temp(relid))
+	{
+		pg_stextdata = table_open(TempStatisticExtDataRelationId,
+								  RowExclusiveLock);
+		cacheId = TEMPSTATEXTDATASTXOID;
+	}
+	else
+	{
+		pg_stextdata = table_open(StatisticExtDataRelationId,
+								  RowExclusiveLock);
+		cacheId = STATEXTDATASTXOID;
+	}
 
-	stxdtup = SearchSysCache2(STATEXTDATASTXOID,
+	stxdtup = SearchSysCache2(cacheId,
 							  values[Anum_pg_statistic_ext_data_stxoid - 1],
 							  values[Anum_pg_statistic_ext_data_stxdinherit - 1]);
 
@@ -749,7 +763,7 @@ extended_statistics_update(FunctionCallInfo fcinfo)
 			success = false;
 	}
 
-	upsert_pg_statistic_ext_data(values, nulls, replaces);
+	upsert_pg_statistic_ext_data(relid, values, nulls, replaces);
 
 cleanup:
 	if (HeapTupleIsValid(tup))
@@ -1700,14 +1714,26 @@ exprs_error:
  * row and "inherited" pair.
  */
 static bool
-delete_pg_statistic_ext_data(Oid stxoid, bool inherited)
+delete_pg_statistic_ext_data(Oid relid, Oid stxoid, bool inherited)
 {
-	Relation	sed = table_open(StatisticExtDataRelationId, RowExclusiveLock);
+	Relation	sed;
+	SysCacheIdentifier cacheId;
 	HeapTuple	oldtup;
 	bool		result = false;
 
+	if (rel_is_global_temp(relid))
+	{
+		sed = table_open(TempStatisticExtDataRelationId, RowExclusiveLock);
+		cacheId = TEMPSTATEXTDATASTXOID;
+	}
+	else
+	{
+		sed = table_open(StatisticExtDataRelationId, RowExclusiveLock);
+		cacheId = STATEXTDATASTXOID;
+	}
+
 	/* Is there already a pg_statistic_ext_data tuple for this attribute? */
-	oldtup = SearchSysCache2(STATEXTDATASTXOID,
+	oldtup = SearchSysCache2(cacheId,
 							 ObjectIdGetDatum(stxoid),
 							 BoolGetDatum(inherited));
 
@@ -1842,7 +1868,7 @@ pg_clear_extended_stats(PG_FUNCTION_ARGS)
 		PG_RETURN_VOID();
 	}
 
-	delete_pg_statistic_ext_data(stxform->oid, inherited);
+	delete_pg_statistic_ext_data(relid, stxform->oid, inherited);
 	heap_freetuple(tup);
 
 	table_close(pg_stext, RowExclusiveLock);
diff --git a/src/backend/statistics/mcv.c b/src/backend/statistics/mcv.c
index 0b7da605a4c..f77b06ca9dc 100644
--- a/src/backend/statistics/mcv.c
+++ b/src/backend/statistics/mcv.c
@@ -553,18 +553,21 @@ build_column_frequencies(SortItem *groups, int ngroups,
  *		Load the MCV list for the indicated pg_statistic_ext_data tuple.
  */
 MCVList *
-statext_mcv_load(Oid mvoid, bool inh)
+statext_mcv_load(Oid relid, Oid mvoid, bool inh)
 {
+	SysCacheIdentifier cacheId;
 	MCVList    *result;
 	bool		isnull;
 	Datum		mcvlist;
-	HeapTuple	htup = SearchSysCache2(STATEXTDATASTXOID,
-									   ObjectIdGetDatum(mvoid), BoolGetDatum(inh));
+	HeapTuple	htup;
 
+	cacheId = rel_is_global_temp(relid) ? TEMPSTATEXTDATASTXOID : STATEXTDATASTXOID;
+
+	htup = SearchSysCache2(cacheId, ObjectIdGetDatum(mvoid), BoolGetDatum(inh));
 	if (!HeapTupleIsValid(htup))
 		elog(ERROR, "cache lookup failed for statistics object %u", mvoid);
 
-	mcvlist = SysCacheGetAttr(STATEXTDATASTXOID, htup,
+	mcvlist = SysCacheGetAttr(cacheId, htup,
 							  Anum_pg_statistic_ext_data_stxdmcv, &isnull);
 
 	if (isnull)
@@ -2058,7 +2061,7 @@ mcv_clauselist_selectivity(PlannerInfo *root, StatisticExtInfo *stat,
 	bool	   *matches = NULL;
 
 	/* load the MCV list stored in the statistics object */
-	mcv = statext_mcv_load(stat->statOid, rte->inh);
+	mcv = statext_mcv_load(rte->relid, stat->statOid, rte->inh);
 
 	/* build a match bitmap for the clauses */
 	matches = mcv_get_match_bitmap(root, clauses, stat->keys, stat->exprs,
diff --git a/src/backend/statistics/mvdistinct.c b/src/backend/statistics/mvdistinct.c
index 4f8f578a22f..d321ef10ba7 100644
--- a/src/backend/statistics/mvdistinct.c
+++ b/src/backend/statistics/mvdistinct.c
@@ -28,6 +28,7 @@
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_statistic_ext_data.h"
 #include "statistics/extended_stats_internal.h"
+#include "utils/lsyscache.h"
 #include "utils/syscache.h"
 #include "utils/typcache.h"
 #include "varatt.h"
@@ -142,19 +143,22 @@ statext_ndistinct_build(double totalrows, StatsBuildData *data)
  *		Load the ndistinct value for the indicated pg_statistic_ext tuple
  */
 MVNDistinct *
-statext_ndistinct_load(Oid mvoid, bool inh)
+statext_ndistinct_load(Oid relid, Oid mvoid, bool inh)
 {
+	SysCacheIdentifier cacheId;
 	MVNDistinct *result;
 	bool		isnull;
 	Datum		ndist;
 	HeapTuple	htup;
 
-	htup = SearchSysCache2(STATEXTDATASTXOID,
+	cacheId = rel_is_global_temp(relid) ? TEMPSTATEXTDATASTXOID : STATEXTDATASTXOID;
+
+	htup = SearchSysCache2(cacheId,
 						   ObjectIdGetDatum(mvoid), BoolGetDatum(inh));
 	if (!HeapTupleIsValid(htup))
 		elog(ERROR, "cache lookup failed for statistics object %u", mvoid);
 
-	ndist = SysCacheGetAttr(STATEXTDATASTXOID, htup,
+	ndist = SysCacheGetAttr(cacheId, htup,
 							Anum_pg_statistic_ext_data_stxdndistinct, &isnull);
 	if (isnull)
 		elog(ERROR,
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 11faeffe118..e05ee05a820 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -4677,7 +4677,7 @@ estimate_multivariate_ndistinct(PlannerInfo *root, RelOptInfo *rel,
 
 	Assert(nmatches_vars + nmatches_exprs > 1);
 
-	stats = statext_ndistinct_load(statOid, rte->inh);
+	stats = statext_ndistinct_load(rte->relid, statOid, rte->inh);
 
 	/*
 	 * If we have a match, search it for the specific item that matches (there
@@ -5929,7 +5929,8 @@ examine_variable(PlannerInfo *root, Node *node, int varRelid,
 					 * Now we just create a new copy every time.
 					 */
 					vardata->statsTuple =
-						statext_expressions_load(info->statOid, rte->inh, pos);
+						statext_expressions_load(rte->relid, info->statOid,
+												 rte->inh, pos);
 
 					/* Nothing to release if no data found */
 					if (vardata->statsTuple != NULL)
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 971e075bff6..dce3a0532dd 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -63,6 +63,7 @@
 #include "catalog/pg_tablespace.h"
 #include "catalog/pg_temp_class.h"
 #include "catalog/pg_temp_statistic.h"
+#include "catalog/pg_temp_statistic_ext_data.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
 #include "catalog/schemapg.h"
diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile
index 6aec03add22..f60a6454df2 100644
--- a/src/include/catalog/Makefile
+++ b/src/include/catalog/Makefile
@@ -88,7 +88,8 @@ CATALOG_HEADERS := \
 	pg_propgraph_label_property.h \
 	pg_propgraph_property.h \
 	pg_temp_class.h \
-	pg_temp_statistic.h
+	pg_temp_statistic.h \
+	pg_temp_statistic_ext_data.h
 
 GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h)
 
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index db809c4b3c0..7599731de7d 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -76,6 +76,7 @@ catalog_headers = [
   'pg_propgraph_property.h',
   'pg_temp_class.h',
   'pg_temp_statistic.h',
+  'pg_temp_statistic_ext_data.h',
 ]
 
 # The .dat files we need can just be listed alphabetically.
diff --git a/src/include/catalog/pg_statistic_ext_data.h b/src/include/catalog/pg_statistic_ext_data.h
index dbc4acc7d1a..e3a6fda0030 100644
--- a/src/include/catalog/pg_statistic_ext_data.h
+++ b/src/include/catalog/pg_statistic_ext_data.h
@@ -30,6 +30,9 @@
  */
 BEGIN_CATALOG_STRUCT
 
+/*
+ * NB: Any changes made here must be reflected in pg_temp_statistic_ext_data.
+ */
 CATALOG(pg_statistic_ext_data,3429,StatisticExtDataRelationId)
 {
 	Oid			stxoid BKI_LOOKUP(pg_statistic_ext);	/* statistics object
diff --git a/src/include/catalog/pg_temp_statistic_ext_data.h b/src/include/catalog/pg_temp_statistic_ext_data.h
new file mode 100644
index 00000000000..eb3b4b2ed04
--- /dev/null
+++ b/src/include/catalog/pg_temp_statistic_ext_data.h
@@ -0,0 +1,62 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_temp_statistic_ext_data.h
+ *	  definition of the "temporary extended statistics data" system catalog
+ *	  (pg_temp_statistic_ext_data)
+ *
+ * This is a global temporary system catalog table storing the statistical
+ * data for extended statistics objects on temporary tables.  Currently, it
+ * is only used for global temporary tables.
+ *
+ * Portions Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * src/include/catalog/pg_temp_statistic_ext_data.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_TEMP_STATISTIC_EXT_DATA_H
+#define PG_TEMP_STATISTIC_EXT_DATA_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_temp_statistic_ext_data_d.h"	/* IWYU pragma: export */
+
+/* ----------------
+ *		pg_temp_statistic_ext_data definition.  cpp turns this into
+ *		typedef struct FormData_pg_temp_statistic_ext_data
+ * ----------------
+ */
+BEGIN_CATALOG_STRUCT
+
+/*
+ * NB: The fields here must exactly match those in pg_statistic_ext_data.
+ */
+CATALOG(pg_temp_statistic_ext_data,8088,TempStatisticExtDataRelationId) BKI_TEMP_RELATION
+{
+	Oid			stxoid BKI_LOOKUP(pg_statistic_ext);	/* statistics object
+														 * this data is for */
+	bool		stxdinherit;	/* true if inheritance children are included */
+
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+
+	pg_ndistinct stxdndistinct; /* ndistinct coefficients (serialized) */
+	pg_dependencies stxddependencies;	/* dependencies (serialized) */
+	pg_mcv_list stxdmcv;		/* MCV (serialized) */
+	pg_statistic stxdexpr[1];	/* stats for expressions */
+
+#endif
+
+} FormData_pg_temp_statistic_ext_data;
+
+END_CATALOG_STRUCT
+
+DECLARE_TOAST(pg_temp_statistic_ext_data, 8089, 8090);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_temp_statistic_ext_data_stxoid_inh_index, 8091, TempStatisticExtDataStxoidInhIndexId, pg_temp_statistic_ext_data, btree(stxoid oid_ops, stxdinherit bool_ops));
+
+MAKE_SYSCACHE(TEMPSTATEXTDATASTXOID, pg_temp_statistic_ext_data_stxoid_inh_index, 4);
+
+#endif							/* PG_TEMP_STATISTIC_EXT_DATA_H */
diff --git a/src/include/commands/defrem.h b/src/include/commands/defrem.h
index d080ad59b71..9edc0a76a75 100644
--- a/src/include/commands/defrem.h
+++ b/src/include/commands/defrem.h
@@ -89,7 +89,7 @@ extern ObjectAddress AlterOperator(AlterOperatorStmt *stmt);
 extern ObjectAddress CreateStatistics(CreateStatsStmt *stmt, bool check_rights);
 extern ObjectAddress AlterStatistics(AlterStatsStmt *stmt);
 extern void RemoveStatisticsById(Oid statsOid);
-extern void RemoveStatisticsDataById(Oid statsOid, bool inh);
+extern void RemoveStatisticsDataById(Oid relid, Oid statsOid, bool inh);
 extern Oid	StatisticsGetRelation(Oid statId, bool missing_ok);
 
 /* commands/aggregatecmds.c */
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 8f9b9d237fd..2ab93adaf82 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -94,9 +94,9 @@ typedef struct MCVList
 	MCVItem		items[FLEXIBLE_ARRAY_MEMBER];	/* array of MCV items */
 } MCVList;
 
-extern MVNDistinct *statext_ndistinct_load(Oid mvoid, bool inh);
-extern MVDependencies *statext_dependencies_load(Oid mvoid, bool inh);
-extern MCVList *statext_mcv_load(Oid mvoid, bool inh);
+extern MVNDistinct *statext_ndistinct_load(Oid relid, Oid mvoid, bool inh);
+extern MVDependencies *statext_dependencies_load(Oid relid, Oid mvoid, bool inh);
+extern MCVList *statext_mcv_load(Oid relid, Oid mvoid, bool inh);
 
 extern void BuildRelationExtStatistics(Relation onerel, bool inh, double totalrows,
 									   int numrows, HeapTuple *rows,
@@ -126,6 +126,6 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 												Bitmapset **clause_attnums,
 												List **clause_exprs,
 												int nclauses);
-extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
+extern HeapTuple statext_expressions_load(Oid relid, Oid stxoid, bool inh, int idx);
 
 #endif							/* STATISTICS_H */
diff --git a/src/test/regress/expected/global_temp.out b/src/test/regress/expected/global_temp.out
index d624f877e3d..374fe83bcd1 100644
--- a/src/test/regress/expected/global_temp.out
+++ b/src/test/regress/expected/global_temp.out
@@ -589,6 +589,77 @@ SELECT row_estimate('SELECT * FROM tmp2 WHERE b = 8');
            17
 (1 row)
 
+DROP TABLE tmp2;
+-- Test extended stats
+CREATE GLOBAL TEMP TABLE tmp2 (a int, b int, c int);
+INSERT INTO tmp2
+  SELECT x, floor(sqrt(x)), floor(sqrt(x)) + 100 FROM generate_series(36, 99) x;
+CREATE STATISTICS tmp2_stats ON b, c FROM tmp2;
+ANALYZE tmp2;
+SELECT stxname,
+       replace(d.stxdndistinct, '}, ', E'},\n') AS stxdndistinct,
+       replace(d.stxddependencies, '}, ', E'},\n') AS stxddependencies
+  FROM pg_statistic_ext s
+  LEFT JOIN pg_statistic_ext_data d ON d.stxoid = s.oid
+ WHERE s.stxname = 'tmp2_stats';
+  stxname   | stxdndistinct | stxddependencies 
+------------+---------------+------------------
+ tmp2_stats |               | 
+(1 row)
+
+SELECT stxname,
+       replace(d.stxdndistinct, '}, ', E'},\n') AS stxdndistinct,
+       replace(d.stxddependencies, '}, ', E'},\n') AS stxddependencies
+  FROM pg_statistic_ext s
+  LEFT JOIN pg_temp_statistic_ext_data d ON d.stxoid = s.oid
+ WHERE s.stxname = 'tmp2_stats';
+  stxname   |              stxdndistinct               |                      stxddependencies                      
+------------+------------------------------------------+------------------------------------------------------------
+ tmp2_stats | [{"attributes": [2, 3], "ndistinct": 4}] | [{"attributes": [2], "dependency": 3, "degree": 1.000000},+
+            |                                          | {"attributes": [3], "dependency": 2, "degree": 1.000000}]
+(1 row)
+
+SELECT m.*
+  FROM pg_statistic_ext s, pg_statistic_ext_data d,
+       pg_mcv_list_items(d.stxdmcv) m
+ WHERE s.stxname = 'tmp2_stats'
+   AND d.stxoid = s.oid;
+ index | values | nulls | frequency | base_frequency 
+-------+--------+-------+-----------+----------------
+(0 rows)
+
+SELECT m.*
+  FROM pg_statistic_ext s, pg_temp_statistic_ext_data d,
+       pg_mcv_list_items(d.stxdmcv) m
+ WHERE s.stxname = 'tmp2_stats'
+   AND d.stxoid = s.oid;
+ index | values  | nulls | frequency | base_frequency 
+-------+---------+-------+-----------+----------------
+     0 | {9,109} | {f,f} |  0.296875 | 0.088134765625
+     1 | {8,108} | {f,f} |  0.265625 | 0.070556640625
+     2 | {7,107} | {f,f} |  0.234375 | 0.054931640625
+     3 | {6,106} | {f,f} |  0.203125 | 0.041259765625
+(4 rows)
+
+SELECT most_common_vals FROM pg_stats_ext
+ WHERE schemaname = 'global_temp_tests' AND tablename = 'tmp2';
+         most_common_vals          
+-----------------------------------
+ {{9,109},{8,108},{7,107},{6,106}}
+(1 row)
+
+SELECT COUNT(*) FROM tmp2 WHERE b = 8 AND c = 108;
+ count 
+-------
+    17
+(1 row)
+
+SELECT row_estimate('SELECT * FROM tmp2 WHERE b = 8 AND c = 108');
+ row_estimate 
+--------------
+           17
+(1 row)
+
 DROP TABLE tmp2;
 -- Test view creation
 INSERT INTO tmp1 VALUES (1, 'xxx');
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index be2efa78257..bccae052a15 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -298,3 +298,4 @@ NOTICE:  checking pg_temp_statistic {stacoll2} => pg_collation {oid}
 NOTICE:  checking pg_temp_statistic {stacoll3} => pg_collation {oid}
 NOTICE:  checking pg_temp_statistic {stacoll4} => pg_collation {oid}
 NOTICE:  checking pg_temp_statistic {stacoll5} => pg_collation {oid}
+NOTICE:  checking pg_temp_statistic_ext_data {stxoid} => pg_statistic_ext {oid}
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index f872091b82c..6288d48c33b 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1367,6 +1367,21 @@ UNION ALL
     pg_temp_statistic.stavalues4,
     pg_temp_statistic.stavalues5
    FROM pg_temp_statistic;
+pg_all_statistic_ext_data| SELECT pg_statistic_ext_data.stxoid,
+    pg_statistic_ext_data.stxdinherit,
+    pg_statistic_ext_data.stxdndistinct,
+    pg_statistic_ext_data.stxddependencies,
+    pg_statistic_ext_data.stxdmcv,
+    pg_statistic_ext_data.stxdexpr
+   FROM pg_statistic_ext_data
+UNION ALL
+ SELECT pg_temp_statistic_ext_data.stxoid,
+    pg_temp_statistic_ext_data.stxdinherit,
+    pg_temp_statistic_ext_data.stxdndistinct,
+    pg_temp_statistic_ext_data.stxddependencies,
+    pg_temp_statistic_ext_data.stxdmcv,
+    pg_temp_statistic_ext_data.stxdexpr
+   FROM pg_temp_statistic_ext_data;
 pg_available_extension_versions| SELECT e.name,
     e.version,
     (x.extname IS NOT NULL) AS installed,
@@ -2783,7 +2798,7 @@ pg_stats_ext| SELECT cn.nspname AS schemaname,
     m.most_common_base_freqs
    FROM (((((pg_statistic_ext s
      JOIN pg_class c ON ((c.oid = s.stxrelid)))
-     JOIN pg_statistic_ext_data sd ON ((s.oid = sd.stxoid)))
+     JOIN pg_all_statistic_ext_data sd ON ((s.oid = sd.stxoid)))
      LEFT JOIN pg_namespace cn ON ((cn.oid = c.relnamespace)))
      LEFT JOIN pg_namespace sn ON ((sn.oid = s.stxnamespace)))
      LEFT JOIN LATERAL ( SELECT array_agg(pg_mcv_list_items."values") AS most_common_vals,
@@ -2886,7 +2901,7 @@ pg_stats_ext_exprs| SELECT cn.nspname AS schemaname,
         END AS range_bounds_histogram
    FROM (((((pg_statistic_ext s
      JOIN pg_class c ON ((c.oid = s.stxrelid)))
-     LEFT JOIN pg_statistic_ext_data sd ON ((s.oid = sd.stxoid)))
+     LEFT JOIN pg_all_statistic_ext_data sd ON ((s.oid = sd.stxoid)))
      LEFT JOIN pg_namespace cn ON ((cn.oid = c.relnamespace)))
      LEFT JOIN pg_namespace sn ON ((sn.oid = s.stxnamespace)))
      JOIN LATERAL ( SELECT unnest(pg_get_statisticsobjdef_expressions(s.oid)) AS expr,
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
index 5f5756d5596..3f14d8f9e8a 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -48,3 +48,27 @@ UNION ALL
 ---------+----------+--------+--------+-----------+----------+----------+----------+------------+----------------+------------+-----------+---------------+-------------+--------------+--------------+------------+-------------+--------------
 (0 rows)
 
+-- check that pg_statistic_ext_data and pg_temp_statistic_ext_data have the
+-- exact same columns
+WITH t1 AS (
+  SELECT attname, atttypid, attlen, attnum, atttypmod, attndims, attbyval,
+         attalign, attstorage, attcompression, attnotnull, atthasdef,
+         atthasmissing, attidentity, attgenerated, attisdropped, attislocal,
+         attinhcount, attcollation
+    FROM pg_attribute
+   WHERE attrelid = 'pg_statistic_ext_data'::regclass
+), t2 AS (
+  SELECT attname, atttypid, attlen, attnum, atttypmod, attndims, attbyval,
+         attalign, attstorage, attcompression, attnotnull, atthasdef,
+         atthasmissing, attidentity, attgenerated, attisdropped, attislocal,
+         attinhcount, attcollation
+    FROM pg_attribute
+   WHERE attrelid = 'pg_temp_statistic_ext_data'::regclass
+)
+(SELECT * FROM t1 EXCEPT SELECT * FROM t2)
+UNION ALL
+(SELECT * FROM t2 EXCEPT SELECT * FROM t1);
+ attname | atttypid | attlen | attnum | atttypmod | attndims | attbyval | attalign | attstorage | attcompression | attnotnull | atthasdef | atthasmissing | attidentity | attgenerated | attisdropped | attislocal | attinhcount | attcollation 
+---------+----------+--------+--------+-----------+----------+----------+----------+------------+----------------+------------+-----------+---------------+-------------+--------------+--------------+------------+-------------+--------------
+(0 rows)
+
diff --git a/src/test/regress/sql/global_temp.sql b/src/test/regress/sql/global_temp.sql
index 2481ec0be56..5563d4f19c7 100644
--- a/src/test/regress/sql/global_temp.sql
+++ b/src/test/regress/sql/global_temp.sql
@@ -347,6 +347,47 @@ SELECT row_estimate('SELECT * FROM tmp2 WHERE b = 8');
 
 DROP TABLE tmp2;
 
+-- Test extended stats
+CREATE GLOBAL TEMP TABLE tmp2 (a int, b int, c int);
+INSERT INTO tmp2
+  SELECT x, floor(sqrt(x)), floor(sqrt(x)) + 100 FROM generate_series(36, 99) x;
+CREATE STATISTICS tmp2_stats ON b, c FROM tmp2;
+ANALYZE tmp2;
+
+SELECT stxname,
+       replace(d.stxdndistinct, '}, ', E'},\n') AS stxdndistinct,
+       replace(d.stxddependencies, '}, ', E'},\n') AS stxddependencies
+  FROM pg_statistic_ext s
+  LEFT JOIN pg_statistic_ext_data d ON d.stxoid = s.oid
+ WHERE s.stxname = 'tmp2_stats';
+
+SELECT stxname,
+       replace(d.stxdndistinct, '}, ', E'},\n') AS stxdndistinct,
+       replace(d.stxddependencies, '}, ', E'},\n') AS stxddependencies
+  FROM pg_statistic_ext s
+  LEFT JOIN pg_temp_statistic_ext_data d ON d.stxoid = s.oid
+ WHERE s.stxname = 'tmp2_stats';
+
+SELECT m.*
+  FROM pg_statistic_ext s, pg_statistic_ext_data d,
+       pg_mcv_list_items(d.stxdmcv) m
+ WHERE s.stxname = 'tmp2_stats'
+   AND d.stxoid = s.oid;
+
+SELECT m.*
+  FROM pg_statistic_ext s, pg_temp_statistic_ext_data d,
+       pg_mcv_list_items(d.stxdmcv) m
+ WHERE s.stxname = 'tmp2_stats'
+   AND d.stxoid = s.oid;
+
+SELECT most_common_vals FROM pg_stats_ext
+ WHERE schemaname = 'global_temp_tests' AND tablename = 'tmp2';
+
+SELECT COUNT(*) FROM tmp2 WHERE b = 8 AND c = 108;
+SELECT row_estimate('SELECT * FROM tmp2 WHERE b = 8 AND c = 108');
+
+DROP TABLE tmp2;
+
 -- Test view creation
 INSERT INTO tmp1 VALUES (1, 'xxx');
 CREATE VIEW v AS SELECT * FROM tmp1;
diff --git a/src/test/regress/sql/sanity_check.sql b/src/test/regress/sql/sanity_check.sql
index ed0096d7823..4bf00feba2c 100644
--- a/src/test/regress/sql/sanity_check.sql
+++ b/src/test/regress/sql/sanity_check.sql
@@ -39,3 +39,24 @@ WITH t1 AS (
 (SELECT * FROM t1 EXCEPT SELECT * FROM t2)
 UNION ALL
 (SELECT * FROM t2 EXCEPT SELECT * FROM t1);
+
+-- check that pg_statistic_ext_data and pg_temp_statistic_ext_data have the
+-- exact same columns
+WITH t1 AS (
+  SELECT attname, atttypid, attlen, attnum, atttypmod, attndims, attbyval,
+         attalign, attstorage, attcompression, attnotnull, atthasdef,
+         atthasmissing, attidentity, attgenerated, attisdropped, attislocal,
+         attinhcount, attcollation
+    FROM pg_attribute
+   WHERE attrelid = 'pg_statistic_ext_data'::regclass
+), t2 AS (
+  SELECT attname, atttypid, attlen, attnum, atttypmod, attndims, attbyval,
+         attalign, attstorage, attcompression, attnotnull, atthasdef,
+         atthasmissing, attidentity, attgenerated, attisdropped, attislocal,
+         attinhcount, attcollation
+    FROM pg_attribute
+   WHERE attrelid = 'pg_temp_statistic_ext_data'::regclass
+)
+(SELECT * FROM t1 EXCEPT SELECT * FROM t2)
+UNION ALL
+(SELECT * FROM t2 EXCEPT SELECT * FROM t1);
-- 
2.51.0

