From 04826695007a28f3ea14981396eaf442dcd5583a Mon Sep 17 00:00:00 2001
From: Andrew Dunstan <andrew@dunslane.net>
Date: Sun, 15 Mar 2026 10:18:26 -0400
Subject: [PATCH 06/12] Global temporary tables: per-session ANALYZE statistics

GTT data is per-session, so the shared pg_class.relpages/reltuples
and pg_statistic rows are meaningless for query planning -- they
reflect one session's sample and mislead every other session.  This
commit adds per-session statistics that ANALYZE populates in
backend-local memory and the planner consults via dedicated lookup
wrappers.

Relation-level stats (relpages, reltuples, relallvisible) are stored
in the existing per-session GTT storage hash via
GttUpdateSessionStats().  plancat.c's estimate_rel_size checks for
per-session stats first and uses them for tuple density estimation,
falling back to the standard attribute-width-based heuristic when
ANALYZE has not been run in this session.

Column-level stats (histograms, MCVs, distinct counts) are stored
as pg_statistic-format HeapTuples in a second backend-local hash
(gtt_colstats_hash), keyed by (relid, attnum, inh).  update_attstats
grows an is_gtt flag: when set, it builds the tuple via a new
build_statstuple() helper and stores it through
GttStoreSessionColumnStats() instead of writing pg_statistic.

The planner read path is changed at 8 call sites (selfuncs.c,
nodeHash.c, lsyscache.c) to use a new SearchStatsWithGtt() wrapper
that checks the per-session hash before falling through to the
pg_statistic syscache.

PreCommit_gtt_on_commit() is extended with a stats_valid reset on
the heap entries that ON COMMIT DELETE ROWS truncated, and calls
gtt_reset_colstats_for_rel() to clear the per-column hash for each
of those relations.  TRUNCATE's session-storage swap resets the
relation-level stats (rolled back with the rest of the swap-undo
record on abort) but keeps column-level stats, matching the behavior
of pg_statistic for regular tables.  Per-session stats do not survive
session end.

ANALYZE builds the per-session stats tuple in the transient context
and copies only the finished tuple into TopMemoryContext, so the
sizable MCV/histogram intermediates are reclaimed with the
transaction instead of accumulating for the backend's life.

index_update_stats() never writes page/tuple counts to the shared
pg_class row for a GTT, for the same reason as vac_update_relstats;
relhasindex, a property of the shared catalog definition, is still
maintained.

Two new SRF functions provide visibility into per-session stats:
- pg_gtt_relstats(regclass) -- relation-level stats
- pg_gtt_colstats(regclass) -- column-level stats (pg_stats-like format)
Both accept NULL to show all GTTs and require the caller to have
SELECT privilege on each relation (and, for colstats, on the
attribute) before its stats are returned, matching pg_stats.  The
regclass DEFAULT NULL is added via system_functions.sql since
regclass is not in the bootstrap TypInfo.

Per-session statistics are transactional: the entry records the
(sub)transaction that last wrote them (stats_subid, reparented on
subtransaction commit like the other subids), and the abort paths
invalidate relation- and column-level stats written by an aborted
(sub)transaction.  Without this, BEGIN; INSERT ...; ANALYZE; ROLLBACK
would leave the planner estimating against row counts for data that no
longer exists, with no autovacuum to ever correct it.
---
 src/backend/catalog/index.c              |  52 ++
 src/backend/catalog/storage_gtt.c        | 771 ++++++++++++++++++++++-
 src/backend/catalog/system_functions.sql |  38 ++
 src/backend/commands/analyze.c           | 302 +++++----
 src/backend/executor/nodeHash.c          |  15 +-
 src/backend/optimizer/util/plancat.c     |  60 ++
 src/backend/utils/adt/selfuncs.c         |  54 +-
 src/backend/utils/cache/lsyscache.c      |  12 +-
 src/include/catalog/pg_proc.dat          |  28 +
 src/include/catalog/storage_gtt.h        |  16 +
 src/tools/pgindent/typedefs.list         |   2 +
 11 files changed, 1194 insertions(+), 156 deletions(-)

diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 9407c357f27..cff8d39fb44 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2343,6 +2343,16 @@ index_drop(Oid indexId, bool concurrent, bool concurrent_lock_mode)
 	if (RELKIND_HAS_STORAGE(userIndexRelation->rd_rel->relkind))
 		RelationDropStorage(userIndexRelation);
 
+	/*
+	 * For a GTT index, also retire this session's storage-map entry (and its
+	 * registry row) at commit, exactly as heap_drop_with_catalog does for
+	 * tables.  Without this the entry would linger with storage_created set
+	 * but no file behind it, tripping any later pass that walks materialized
+	 * entries.
+	 */
+	if (RelationIsGlobalTemp(userIndexRelation))
+		GttScheduleDropSessionStorage(indexId);
+
 	/* ensure that stats are dropped if transaction commits */
 	pgstat_drop_relation(userIndexRelation);
 
@@ -2880,6 +2890,18 @@ index_update_stats(Relation rel,
 			update_stats = false;
 	}
 
+	/*
+	 * For a global temporary table, the page/tuple counts describe this
+	 * session's private data and must never be written to the shared pg_class
+	 * row, which is common to all sessions (cf. vac_update_relstats).  Just
+	 * drop them: per-session statistics are established by ANALYZE, and until
+	 * then the planner estimates from the session storage's actual size, as
+	 * for any fresh table.  relhasindex is a property of the shared catalog
+	 * definition, so fall through to update it below.
+	 */
+	if (RelationIsGlobalTemp(rel))
+		update_stats = false;
+
 	/*
 	 * Finish I/O and visibility map buffer locks before
 	 * systable_inplace_update_begin() locks the pg_class buffer.  The rd_rel
@@ -3038,6 +3060,36 @@ index_build(Relation heapRelation,
 	Assert(indexRelation->rd_indam->ambuild);
 	Assert(indexRelation->rd_indam->ambuildempty);
 
+	/*
+	 * A GTT index's per-session storage is created lazily.  If the parent
+	 * heap has no per-session storage yet, defer the physical build entirely:
+	 * there is nothing to index, and materializing the index now would let a
+	 * later transaction's rollback strand its entries (the heap file is
+	 * unlinked on abort, but an index file committed earlier is not -- though
+	 * gtt_truncate_dependents also backstops that case). The catalog work has
+	 * already happened; the per-session structure is built when the heap
+	 * materializes, or at the first index scan. Otherwise (heap has storage),
+	 * materialize the index and build for real -- covering CREATE INDEX on a
+	 * populated GTT and the in-place truncation of a same-transaction-created
+	 * GTT.
+	 */
+	if (RelationIsGlobalTemp(indexRelation))
+	{
+		if (!GttHasSessionStorage(RelationGetRelid(heapRelation)))
+		{
+			/*
+			 * Even a deferred build must mark the shared catalog: without
+			 * relhasindex the planner never looks at pg_index, in every
+			 * session.  (index_update_stats writes no page/tuple counts for a
+			 * GTT; relhasindex is shared-definition state.)
+			 */
+			GttMarkIndexBuildDeferred(indexRelation);
+			index_update_stats(heapRelation, true, -1);
+			return;
+		}
+		GttEnsureSessionStorage(indexRelation);
+	}
+
 	/*
 	 * Determine worker process details for parallel CREATE INDEX.  Currently,
 	 * only btree, GIN, and BRIN have support for parallel builds.
diff --git a/src/backend/catalog/storage_gtt.c b/src/backend/catalog/storage_gtt.c
index bd4540b59b1..fc6feb42f6d 100644
--- a/src/backend/catalog/storage_gtt.c
+++ b/src/backend/catalog/storage_gtt.c
@@ -22,6 +22,7 @@
 #include "postgres.h"
 
 #include "access/amapi.h"
+#include "access/htup_details.h"
 #include "access/parallel.h"
 #include "access/relation.h"
 #include "access/table.h"
@@ -29,24 +30,33 @@
 #include "access/xact.h"
 #include "catalog/heap.h"
 #include "catalog/index.h"
+#include "catalog/pg_attribute.h"
+#include "catalog/pg_statistic.h"
 #include "catalog/pg_tablespace_d.h"
 #include "catalog/storage.h"
 #include "catalog/storage_gtt.h"
 #include "commands/sequence.h"
 #include "commands/tablecmds.h"
 #include "common/hashfn.h"
+#include "funcapi.h"
 #include "miscadmin.h"
 #include "nodes/pg_list.h"
 #include "storage/ipc.h"
 #include "storage/bufmgr.h"
 #include "storage/procnumber.h"
 #include "storage/smgr.h"
+#include "utils/acl.h"
+#include "utils/array.h"
+#include "utils/fmgroids.h"
+#include "utils/builtins.h"
+#include "utils/tuplestore.h"
 #include "utils/hsearch.h"
 #include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
 #include "utils/relcache.h"
+#include "utils/syscache.h"
 
 /*
  * Per-session state for a single global temporary table.
@@ -58,6 +68,7 @@
  *     commit)
  *   - storage_subid: subxact that most recently called RelationCreateStorage
  *   - index_subid: subxact that built the index for this session
+ *   - stats_subid: subxact that last wrote per-session statistics
  * On subxact or xact abort of a given subid, the corresponding state is
  * reverted.  On subxact commit, the subid is reparented.  See
  * gtt_subxact_callback / gtt_xact_callback.
@@ -80,6 +91,13 @@ typedef struct GttStorageEntry
 	SubTransactionId create_subid;	/* subxact that added this entry */
 	SubTransactionId storage_subid; /* subxact that created current storage */
 	SubTransactionId index_subid;	/* subxact that built the index */
+	SubTransactionId stats_subid;	/* subxact that last wrote session stats */
+
+	/* Per-session relation statistics (set by ANALYZE) */
+	bool		stats_valid;	/* has ANALYZE been run in this session? */
+	BlockNumber relpages;		/* per-session page count */
+	float4		reltuples;		/* per-session tuple count */
+	BlockNumber relallvisible;	/* per-session all-visible pages */
 } GttStorageEntry;
 
 /* Backend-local hash table: GTT OID -> GttStorageEntry */
@@ -110,17 +128,55 @@ typedef struct GttSwapUndo
 	RelFileNumber prev_relnumber;	/* mapping to restore on abort */
 	bool		prev_index_built;
 	bool		prev_build_deferred;
+	bool		prev_stats_valid;
+	BlockNumber prev_relpages;
+	float4		prev_reltuples;
+	BlockNumber prev_relallvisible;
 } GttSwapUndo;
 
 /* List of GttSwapUndo *, newest first, allocated in TopMemoryContext */
 static List *gtt_swap_undo = NIL;
 
+/*
+ * Per-session column-level statistics for global temporary tables.
+ *
+ * Column stats (histograms, MCVs, distinct counts, etc.) are stored as
+ * pg_statistic-format HeapTuples in a separate backend-local hash table,
+ * keyed by (relid, attnum, inh).  This parallels the relation-level stats
+ * (relpages/reltuples) stored in GttStorageEntry above.
+ *
+ * The key has trailing alignment padding that HASH_BLOBS hashes verbatim,
+ * so all key instances must be zero-initialized before the fields are set.
+ * Always build keys via init_colstats_key() rather than by hand.
+ */
+typedef struct GttColStatsKey
+{
+	Oid			relid;			/* relation OID */
+	AttrNumber	attnum;			/* attribute number */
+	bool		inh;			/* include inheritance children? */
+} GttColStatsKey;
+
+typedef struct GttColStatsEntry
+{
+	GttColStatsKey key;			/* hash key — must be first */
+	HeapTuple	statsTuple;		/* pg_statistic-format tuple in
+								 * TopMemoryContext */
+} GttColStatsEntry;
+
+/* Backend-local hash table: (relid, attnum, inh) -> GttColStatsEntry */
+static HTAB *gtt_colstats_hash = NULL;
+
 /* Guard against recursive index builds */
 static bool gtt_building_index = false;
 
 /* Local function prototypes */
 static void gtt_session_cleanup(int code, Datum arg);
 static void ensure_gtt_hash(void);
+static void ensure_gtt_colstats_hash(void);
+static void init_colstats_key(GttColStatsKey *key, Oid relid,
+							  AttrNumber attnum, bool inh);
+static void gtt_reset_colstats_for_rel(Oid relid);
+static char *format_stats_values_as_text(AttStatsSlot *sslot);
 static void gtt_xact_callback(XactEvent event, void *arg);
 static void gtt_subxact_callback(SubXactEvent event,
 								 SubTransactionId mySubid,
@@ -166,6 +222,48 @@ ensure_gtt_hash(void)
 	RegisterSubXactCallback(gtt_subxact_callback, NULL);
 }
 
+/*
+ * ensure_gtt_colstats_hash
+ *		Create the backend-local column stats hash table on first use.
+ *
+ * This is separate from ensure_gtt_hash() so the column stats hash is only
+ * created when actually needed (during ANALYZE or planner lookup).
+ */
+static void
+ensure_gtt_colstats_hash(void)
+{
+	HASHCTL		hashctl;
+
+	if (gtt_colstats_hash != NULL)
+		return;
+
+	hashctl.keysize = sizeof(GttColStatsKey);
+	hashctl.entrysize = sizeof(GttColStatsEntry);
+	hashctl.hcxt = TopMemoryContext;
+	gtt_colstats_hash = hash_create("GTT column stats hash",
+									64, /* initial size */
+									&hashctl,
+									HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+}
+
+/*
+ * init_colstats_key
+ *		Build a GttColStatsKey with deterministic byte contents.
+ *
+ * HASH_BLOBS hashes the key byte-for-byte, including any trailing
+ * alignment padding the compiler may insert after the last field.
+ * Zero the whole struct first so equivalent (relid, attnum, inh) triples
+ * always produce the same hash.
+ */
+static void
+init_colstats_key(GttColStatsKey *key, Oid relid, AttrNumber attnum, bool inh)
+{
+	memset(key, 0, sizeof(*key));
+	key->relid = relid;
+	key->attnum = attnum;
+	key->inh = inh;
+}
+
 /*
  * GttInitSessionStorage
  *		Ensure per-session local storage exists for the given GTT relation.
@@ -334,8 +432,20 @@ gtt_init_entry(GttStorageEntry *entry, Relation relation)
 	gtt_xact_state_dirty = true;
 	entry->storage_subid = InvalidSubTransactionId;
 	entry->index_subid = InvalidSubTransactionId;
+	entry->stats_subid = InvalidSubTransactionId;
+	entry->stats_valid = false;
+	entry->relpages = 0;
+	entry->reltuples = 0;
+	entry->relallvisible = 0;
 	entry->on_commit_delete = false;
 	entry->toast_relid = InvalidOid;
+
+	/*
+	 * Discard any column statistics recorded under this OID: when the entry
+	 * is being refreshed after OID recycling, they describe a different,
+	 * dropped relation.  (For a brand-new entry this is a no-op.)
+	 */
+	gtt_reset_colstats_for_rel(entry->relid);
 }
 
 /*
@@ -458,6 +568,10 @@ GttSetNewSessionRelfilenumber(Relation relation, RelFileNumber newrelfilenumber)
 	undo->prev_relnumber = entry->locator.relNumber;
 	undo->prev_index_built = entry->index_built;
 	undo->prev_build_deferred = entry->build_deferred;
+	undo->prev_stats_valid = entry->stats_valid;
+	undo->prev_relpages = entry->relpages;
+	undo->prev_reltuples = entry->reltuples;
+	undo->prev_relallvisible = entry->relallvisible;
 	gtt_swap_undo = lcons(undo, gtt_swap_undo);
 	MemoryContextSwitchTo(oldcxt);
 
@@ -466,7 +580,9 @@ GttSetNewSessionRelfilenumber(Relation relation, RelFileNumber newrelfilenumber)
 
 	/*
 	 * The new file is empty: indexes must be lazily rebuilt on next access
-	 * (GttBuildIndexIfNeeded).
+	 * (GttBuildIndexIfNeeded), and previous ANALYZE results no longer apply.
+	 * Column-level statistics are left alone, matching the behavior of
+	 * TRUNCATE on regular tables, which does not clear pg_statistic.
 	 */
 	if (entry->is_index)
 	{
@@ -482,6 +598,10 @@ GttSetNewSessionRelfilenumber(Relation relation, RelFileNumber newrelfilenumber)
 		if (relation->rd_createSubid != InvalidSubTransactionId)
 			entry->build_deferred = true;
 	}
+	entry->stats_valid = false;
+	entry->relpages = 0;
+	entry->reltuples = 0;
+	entry->relallvisible = 0;
 
 	/* Point the open relcache entry at the new storage. */
 	relation->rd_locator = entry->locator;
@@ -509,6 +629,10 @@ gtt_swap_undo_apply(GttSwapUndo *undo)
 	entry->locator.relNumber = undo->prev_relnumber;
 	entry->index_built = undo->prev_index_built;
 	entry->build_deferred = undo->prev_build_deferred;
+	entry->stats_valid = undo->prev_stats_valid;
+	entry->relpages = undo->prev_relpages;
+	entry->reltuples = undo->prev_reltuples;
+	entry->relallvisible = undo->prev_relallvisible;
 
 	/*
 	 * Refresh the relcache entry so rd_locator points back at the surviving
@@ -577,6 +701,9 @@ gtt_remove_entry(GttStorageEntry *entry)
 {
 	Oid			relid = entry->relid;
 
+	/* Discard any per-session column statistics for this relation */
+	gtt_reset_colstats_for_rel(relid);
+
 	hash_search(gtt_storage_hash, &relid, HASH_REMOVE, NULL);
 }
 
@@ -750,6 +877,7 @@ gtt_xact_callback(XactEvent event, void *arg)
 				entry->create_subid = InvalidSubTransactionId;
 				entry->storage_subid = InvalidSubTransactionId;
 				entry->index_subid = InvalidSubTransactionId;
+				entry->stats_subid = InvalidSubTransactionId;
 			}
 		}
 		else
@@ -780,6 +908,20 @@ gtt_xact_callback(XactEvent event, void *arg)
 					entry->index_built = false;
 					entry->index_subid = InvalidSubTransactionId;
 				}
+				if (entry->stats_subid != InvalidSubTransactionId)
+				{
+					/*
+					 * Session statistics written by the aborted transaction
+					 * describe rolled-back data; throw them away (column
+					 * stats too -- the previous tuples were freed when the
+					 * aborted ANALYZE replaced them, so there is nothing to
+					 * restore).  The planner falls back to size-based
+					 * estimation, which is right for the surviving state.
+					 */
+					entry->stats_valid = false;
+					entry->stats_subid = InvalidSubTransactionId;
+					gtt_reset_colstats_for_rel(entry->relid);
+				}
 				entry->drop_pending = false;
 			}
 		}
@@ -863,6 +1005,8 @@ gtt_subxact_callback(SubXactEvent event,
 				entry->storage_subid = parentSubid;
 			if (entry->index_subid == mySubid)
 				entry->index_subid = parentSubid;
+			if (entry->stats_subid == mySubid)
+				entry->stats_subid = parentSubid;
 		}
 		else					/* SUBXACT_EVENT_ABORT_SUB */
 		{
@@ -882,6 +1026,13 @@ gtt_subxact_callback(SubXactEvent event,
 				entry->index_built = false;
 				entry->index_subid = InvalidSubTransactionId;
 			}
+			if (entry->stats_subid == mySubid)
+			{
+				/* see gtt_xact_callback */
+				entry->stats_valid = false;
+				entry->stats_subid = InvalidSubTransactionId;
+				gtt_reset_colstats_for_rel(entry->relid);
+			}
 		}
 	}
 
@@ -1185,6 +1336,285 @@ GttPrepareIndexAccess(Relation indexRelation)
 	gtt_build_index_internal(indexRelation, true);
 }
 
+/*
+ * GttGetSessionStats
+ *		Retrieve per-session relation statistics for a GTT.
+ *
+ * Returns true if per-session statistics are available (i.e. ANALYZE has
+ * been run on this GTT in this session), filling in the output parameters.
+ * Returns false if no per-session stats exist, in which case the planner
+ * should fall back to default estimation.
+ */
+bool
+GttGetSessionStats(Oid relid, BlockNumber *relpages, double *reltuples,
+				   BlockNumber *relallvisible)
+{
+	GttStorageEntry *entry;
+
+	if (gtt_storage_hash == NULL)
+		return false;
+
+	entry = (GttStorageEntry *) hash_search(gtt_storage_hash,
+											&relid,
+											HASH_FIND,
+											NULL);
+	if (entry == NULL || !entry->stats_valid)
+		return false;
+
+	*relpages = entry->relpages;
+	*reltuples = (double) entry->reltuples;
+	*relallvisible = entry->relallvisible;
+	return true;
+}
+
+/*
+ * GttUpdateSessionStats
+ *		Store per-session relation statistics for a GTT.
+ *
+ * Called from ANALYZE to record relpages/reltuples/relallvisible in the
+ * per-session hash instead of writing to the shared pg_class row.
+ */
+void
+GttUpdateSessionStats(Oid relid, BlockNumber relpages, double reltuples,
+					  BlockNumber relallvisible)
+{
+	GttStorageEntry *entry;
+
+	if (gtt_storage_hash == NULL)
+		return;
+
+	entry = (GttStorageEntry *) hash_search(gtt_storage_hash,
+											&relid,
+											HASH_FIND,
+											NULL);
+	if (entry == NULL)
+		return;
+
+	entry->stats_valid = true;
+	entry->relpages = relpages;
+	entry->reltuples = (float4) reltuples;
+	entry->relallvisible = relallvisible;
+
+	/*
+	 * Unlike pg_class/pg_statistic writes, these survive a transaction abort
+	 * unless we act: remember the writing subxact so the abort paths can
+	 * invalidate stats that describe rolled-back data.
+	 */
+	entry->stats_subid = GetCurrentSubTransactionId();
+	gtt_xact_state_dirty = true;
+}
+
+/*
+ * GttResetSessionStats
+ *		Invalidate per-session stats for a GTT after TRUNCATE.
+ *
+ * After truncation, the previous ANALYZE statistics are no longer valid.
+ * The planner will fall back to default estimation based on actual page
+ * count until ANALYZE is run again.
+ */
+void
+GttResetSessionStats(Oid relid)
+{
+	GttStorageEntry *entry;
+
+	if (gtt_storage_hash == NULL)
+		return;
+
+	entry = (GttStorageEntry *) hash_search(gtt_storage_hash,
+											&relid,
+											HASH_FIND,
+											NULL);
+	if (entry != NULL)
+		entry->stats_valid = false;
+
+	/* Also clear any per-session column statistics */
+	gtt_reset_colstats_for_rel(relid);
+}
+
+/*
+ * GttStoreSessionColumnStats
+ *		Store a per-session column statistics tuple for a GTT.
+ *
+ * The tuple must be a pg_statistic-format HeapTuple allocated in
+ * TopMemoryContext.  If an entry already exists for this (relid, attnum, inh),
+ * the old tuple is freed and replaced.
+ */
+void
+GttStoreSessionColumnStats(Oid relid, AttrNumber attnum, bool inh,
+						   HeapTuple tuple)
+{
+	GttColStatsKey key;
+	GttColStatsEntry *entry;
+	GttStorageEntry *rel_entry;
+	bool		found;
+
+	ensure_gtt_colstats_hash();
+
+	init_colstats_key(&key, relid, attnum, inh);
+
+	entry = (GttColStatsEntry *) hash_search(gtt_colstats_hash,
+											 &key,
+											 HASH_ENTER,
+											 &found);
+	if (found && entry->statsTuple != NULL)
+		heap_freetuple(entry->statsTuple);
+
+	entry->statsTuple = tuple;
+
+	/*
+	 * Mark the relation-level entry so an abort of the writing (sub)xact
+	 * invalidates the column stats along with the relation stats; see
+	 * GttUpdateSessionStats.
+	 */
+	if (gtt_storage_hash != NULL)
+	{
+		rel_entry = (GttStorageEntry *) hash_search(gtt_storage_hash, &relid,
+													HASH_FIND, NULL);
+		if (rel_entry != NULL)
+		{
+			rel_entry->stats_subid = GetCurrentSubTransactionId();
+			gtt_xact_state_dirty = true;
+		}
+	}
+}
+
+/*
+ * GttSearchColumnStats
+ *		Look up per-session column statistics for a GTT column.
+ *
+ * Returns the stored pg_statistic-format HeapTuple, or NULL if no per-session
+ * stats exist for this column.  The caller must NOT free the returned tuple;
+ * it is owned by the hash table.
+ */
+HeapTuple
+GttSearchColumnStats(Oid relid, AttrNumber attnum, bool inh)
+{
+	GttColStatsKey key;
+	GttColStatsEntry *entry;
+
+	if (gtt_colstats_hash == NULL)
+		return NULL;
+
+	init_colstats_key(&key, relid, attnum, inh);
+
+	entry = (GttColStatsEntry *) hash_search(gtt_colstats_hash,
+											 &key,
+											 HASH_FIND,
+											 NULL);
+	if (entry != NULL)
+		return entry->statsTuple;
+
+	return NULL;
+}
+
+/*
+ * GttReleaseColumnStats
+ *		No-op freefunc for per-session GTT column statistics tuples.
+ *
+ * The tuple is owned by gtt_colstats_hash and must not be freed by the
+ * planner.  This function is used as the VariableStatData.freefunc callback.
+ */
+void
+GttReleaseColumnStats(HeapTuple tuple)
+{
+	/* No-op: tuple lives in gtt_colstats_hash in TopMemoryContext */
+}
+
+/*
+ * SearchStats
+ *		Look up column statistics, checking per-session GTT stats if requested.
+ *
+ * Checks the pg_statistic syscache first.  If include_gtt is true and no
+ * shared stats are found, falls back to per-session GTT statistics.
+ * Sets *freefunc to the appropriate release function for the returned tuple.
+ */
+HeapTuple
+SearchStats(Oid relid, AttrNumber attnum, bool inh,
+			bool include_gtt,
+			void (**freefunc) (HeapTuple))
+{
+	HeapTuple	tuple;
+
+	/*
+	 * Check the shared pg_statistic catalog first.  A GTT never has rows
+	 * there (ANALYZE diverts its stats to the per-session hash), so a
+	 * syscache hit settles the lookup without touching the GTT hash: once any
+	 * GTT has been ANALYZEd in this session, probing the hash first would
+	 * cost every planner stats lookup for ordinary analyzed tables a
+	 * guaranteed-miss hash search on this hot path.
+	 */
+	tuple = SearchSysCache3(STATRELATTINH,
+							ObjectIdGetDatum(relid),
+							Int16GetDatum(attnum),
+							BoolGetDatum(inh));
+	if (HeapTupleIsValid(tuple))
+	{
+		*freefunc = ReleaseSysCache;
+		return tuple;
+	}
+
+	/* No shared stats: per-session GTT stats, or no stats at all. */
+	if (include_gtt)
+	{
+		tuple = GttSearchColumnStats(relid, attnum, inh);
+		if (HeapTupleIsValid(tuple))
+		{
+			*freefunc = GttReleaseColumnStats;
+			return tuple;
+		}
+	}
+
+	*freefunc = ReleaseSysCache;
+	return NULL;
+}
+
+/*
+ * gtt_reset_colstats_for_rel
+ *		Remove all per-session column statistics for a given relation.
+ *
+ * Used when stats are invalidated (TRUNCATE, ON COMMIT DELETE ROWS, DROP).
+ */
+static void
+gtt_reset_colstats_for_rel(Oid relid)
+{
+	HASH_SEQ_STATUS status;
+	GttColStatsEntry *entry;
+	List	   *keys_to_remove = NIL;
+	ListCell   *lc;
+
+	if (gtt_colstats_hash == NULL)
+		return;
+
+	/*
+	 * Collect matching keys first; we can't remove hash entries during an
+	 * active hash_seq_search scan.
+	 */
+	hash_seq_init(&status, gtt_colstats_hash);
+	while ((entry = (GttColStatsEntry *) hash_seq_search(&status)) != NULL)
+	{
+		GttColStatsKey *keycopy;
+
+		if (entry->key.relid != relid)
+			continue;
+
+		keycopy = (GttColStatsKey *) palloc(sizeof(*keycopy));
+		*keycopy = entry->key;
+		keys_to_remove = lappend(keys_to_remove, keycopy);
+	}
+
+	foreach(lc, keys_to_remove)
+	{
+		GttColStatsKey *key = (GttColStatsKey *) lfirst(lc);
+
+		entry = (GttColStatsEntry *) hash_search(gtt_colstats_hash, key,
+												 HASH_REMOVE, NULL);
+		if (entry != NULL && entry->statsTuple != NULL)
+			heap_freetuple(entry->statsTuple);
+		pfree(key);
+	}
+	list_free(keys_to_remove);
+}
+
 /*
  * PreCommit_gtt_on_commit
  *		Truncate ON COMMIT DELETE ROWS GTTs at commit.
@@ -1218,7 +1648,7 @@ PreCommit_gtt_on_commit(void)
 	if (!(MyXactFlags & XACT_FLAGS_ACCESSEDTEMPNAMESPACE))
 		return;
 
-	/* First pass: identify ON COMMIT DELETE ROWS heaps to truncate. */
+	/* First pass: reset heap entries and remember which heaps were wiped. */
 	hash_seq_init(&status, gtt_storage_hash);
 	while ((entry = (GttStorageEntry *) hash_seq_search(&status)) != NULL)
 	{
@@ -1239,6 +1669,7 @@ PreCommit_gtt_on_commit(void)
 						MAIN_FORKNUM) == 0)
 			continue;
 
+		entry->stats_valid = false;
 		heap_relids = lappend_oid(heap_relids, entry->relid);
 
 		/*
@@ -1282,6 +1713,10 @@ PreCommit_gtt_on_commit(void)
 			gtt_truncate_smgr(entry);
 	}
 
+	/* Column stats live in a second hash; clear them for each truncated rel. */
+	foreach_oid(relid, heap_relids)
+		gtt_reset_colstats_for_rel(relid);
+
 	list_free(heap_relids);
 }
 
@@ -1372,6 +1807,13 @@ gtt_session_cleanup(int code, Datum arg)
 	HASH_SEQ_STATUS status;
 	GttStorageEntry *entry;
 
+	/*
+	 * The column-stats hash lives in TopMemoryContext and will be torn down
+	 * with the rest of process memory shortly after we return; nothing to do
+	 * here.  We only walk gtt_storage_hash because each entry owns
+	 * externally-visible resources (on-disk files and a session lock) that
+	 * must be released explicitly.
+	 */
 	if (gtt_storage_hash == NULL)
 		return;
 
@@ -1387,3 +1829,328 @@ gtt_session_cleanup(int code, Datum arg)
 		}
 	}
 }
+
+/*
+ * format_stats_values_as_text
+ *		Convert the values from an AttStatsSlot into a text representation.
+ *
+ * We build a PostgreSQL array of the slot's element type and return its
+ * array_out textual form.  That gives proper array-literal escaping for
+ * values containing commas, braces, double quotes, backslashes, etc.,
+ * matching what pg_stats produces via its anyarray columns.
+ */
+static char *
+format_stats_values_as_text(AttStatsSlot *sslot)
+{
+	ArrayType  *arr;
+	int16		typlen;
+	bool		typbyval;
+	char		typalign;
+
+	get_typlenbyvalalign(sslot->valuetype, &typlen, &typbyval, &typalign);
+	arr = construct_array(sslot->values, sslot->nvalues,
+						  sslot->valuetype, typlen, typbyval, typalign);
+
+	return OidOutputFunctionCall(F_ARRAY_OUT, PointerGetDatum(arr));
+}
+
+/*
+ * pg_gtt_relstats
+ *		Return per-session relation-level statistics for global temporary tables.
+ *
+ * If a regclass argument is provided, returns stats only for that table.
+ * If NULL (the default), returns stats for all GTTs with valid session stats.
+ */
+Datum
+pg_gtt_relstats(PG_FUNCTION_ARGS)
+{
+#define PG_GTT_SESSION_RELSTATS_COLS 5
+	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+	Oid			filter_relid = InvalidOid;
+	HASH_SEQ_STATUS status;
+	GttStorageEntry *entry;
+	Datum		values[PG_GTT_SESSION_RELSTATS_COLS];
+	bool		nulls[PG_GTT_SESSION_RELSTATS_COLS];
+
+	if (!PG_ARGISNULL(0))
+		filter_relid = PG_GETARG_OID(0);
+
+	InitMaterializedSRF(fcinfo, 0);
+
+	if (gtt_storage_hash == NULL)
+		return (Datum) 0;
+
+	memset(nulls, 0, sizeof(nulls));
+
+	hash_seq_init(&status, gtt_storage_hash);
+	while ((entry = (GttStorageEntry *) hash_seq_search(&status)) != NULL)
+	{
+		char	   *relname;
+
+		if (!entry->stats_valid)
+			continue;
+		if (OidIsValid(filter_relid) && entry->relid != filter_relid)
+			continue;
+
+		/*
+		 * Respect SELECT privilege on the target relation so callers can't
+		 * inspect stats for relations they can't see.  Mirrors pg_stats.
+		 */
+		if (pg_class_aclcheck(entry->relid, GetUserId(), ACL_SELECT) != ACLCHECK_OK)
+			continue;
+
+		relname = get_rel_name(entry->relid);
+		if (relname == NULL)
+			continue;
+
+		values[0] = ObjectIdGetDatum(entry->relid);
+		values[1] = CStringGetTextDatum(relname);
+		values[2] = Int32GetDatum((int32) entry->relpages);
+		values[3] = Float4GetDatum(entry->reltuples);
+		values[4] = Int32GetDatum((int32) entry->relallvisible);
+
+		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
+							 values, nulls);
+	}
+
+	return (Datum) 0;
+}
+
+/*
+ * pg_gtt_colstats
+ *		Return per-session column-level statistics for global temporary tables.
+ *
+ * Returns stats in a format similar to the pg_stats view: scalar stats
+ * (null_frac, avg_width, n_distinct), MCVs, histograms, and correlation.
+ * Array-typed values are converted to text representation.
+ *
+ * If a regclass argument is provided, returns stats only for that table.
+ * If NULL (the default), returns stats for all GTTs with column stats.
+ */
+Datum
+pg_gtt_colstats(PG_FUNCTION_ARGS)
+{
+#define PG_GTT_SESSION_COLSTATS_COLS 12
+	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+	Oid			filter_relid = InvalidOid;
+	HASH_SEQ_STATUS status;
+	GttColStatsEntry *csentry;
+
+	if (!PG_ARGISNULL(0))
+		filter_relid = PG_GETARG_OID(0);
+
+	InitMaterializedSRF(fcinfo, 0);
+
+	if (gtt_colstats_hash == NULL)
+		return (Datum) 0;
+
+	hash_seq_init(&status, gtt_colstats_hash);
+	while ((csentry = (GttColStatsEntry *) hash_seq_search(&status)) != NULL)
+	{
+		HeapTuple	statstuple = csentry->statsTuple;
+		Form_pg_statistic stats;
+		char	   *relname;
+		char	   *attname;
+		int			mcv_slot;
+		int			hist_slot;
+		int			corr_slot;
+		int			k;
+		Datum		values[PG_GTT_SESSION_COLSTATS_COLS];
+		bool		nulls[PG_GTT_SESSION_COLSTATS_COLS];
+
+		if (statstuple == NULL)
+			continue;
+		if (OidIsValid(filter_relid) && csentry->key.relid != filter_relid)
+			continue;
+
+		/*
+		 * Require column-level SELECT privilege (or table-level) on the
+		 * attribute to see its stats, matching pg_stats behavior.
+		 */
+		if (pg_class_aclcheck(csentry->key.relid, GetUserId(),
+							  ACL_SELECT) != ACLCHECK_OK &&
+			pg_attribute_aclcheck(csentry->key.relid, csentry->key.attnum,
+								  GetUserId(), ACL_SELECT) != ACLCHECK_OK)
+			continue;
+
+		relname = get_rel_name(csentry->key.relid);
+		if (relname == NULL)
+			continue;
+
+		stats = (Form_pg_statistic) GETSTRUCT(statstuple);
+
+		/* Start with all nulls, then fill in non-null columns */
+		memset(nulls, true, sizeof(nulls));
+
+		values[0] = ObjectIdGetDatum(csentry->key.relid);
+		nulls[0] = false;
+		values[1] = CStringGetTextDatum(relname);
+		nulls[1] = false;
+		values[2] = Int16GetDatum(csentry->key.attnum);
+		nulls[2] = false;
+
+		attname = get_attname(csentry->key.relid, csentry->key.attnum, true);
+		if (attname != NULL)
+		{
+			values[3] = CStringGetTextDatum(attname);
+			nulls[3] = false;
+		}
+
+		values[4] = BoolGetDatum(csentry->key.inh);
+		nulls[4] = false;
+		values[5] = Float4GetDatum(stats->stanullfrac);
+		nulls[5] = false;
+		values[6] = Int32GetDatum(stats->stawidth);
+		nulls[6] = false;
+		values[7] = Float4GetDatum(stats->stadistinct);
+		nulls[7] = false;
+
+		/* Find which slots contain MCV, histogram, and correlation */
+		mcv_slot = -1;
+		hist_slot = -1;
+		corr_slot = -1;
+		for (k = 0; k < STATISTIC_NUM_SLOTS; k++)
+		{
+			int16		kind = (&stats->stakind1)[k];
+
+			if (kind == STATISTIC_KIND_MCV)
+				mcv_slot = k;
+			else if (kind == STATISTIC_KIND_HISTOGRAM)
+				hist_slot = k;
+			else if (kind == STATISTIC_KIND_CORRELATION)
+				corr_slot = k;
+		}
+
+		/* MCV values (as text) and frequencies (as float4[]) */
+		if (mcv_slot >= 0)
+		{
+			AttStatsSlot sslot;
+
+			if (get_attstatsslot(&sslot, statstuple, STATISTIC_KIND_MCV,
+								 InvalidOid,
+								 ATTSTATSSLOT_VALUES | ATTSTATSSLOT_NUMBERS))
+			{
+				values[8] = CStringGetTextDatum(
+												format_stats_values_as_text(&sslot));
+				nulls[8] = false;
+
+				if (sslot.nnumbers > 0)
+				{
+					Datum	   *num_datums;
+					int			j;
+
+					num_datums = (Datum *) palloc(sslot.nnumbers * sizeof(Datum));
+					for (j = 0; j < sslot.nnumbers; j++)
+						num_datums[j] = Float4GetDatum(sslot.numbers[j]);
+					values[9] = PointerGetDatum(
+												construct_array_builtin(num_datums, sslot.nnumbers,
+																		FLOAT4OID));
+					nulls[9] = false;
+					pfree(num_datums);
+				}
+
+				free_attstatsslot(&sslot);
+			}
+		}
+
+		/* Histogram bounds (as text) */
+		if (hist_slot >= 0)
+		{
+			AttStatsSlot sslot;
+
+			if (get_attstatsslot(&sslot, statstuple, STATISTIC_KIND_HISTOGRAM,
+								 InvalidOid, ATTSTATSSLOT_VALUES))
+			{
+				values[10] = CStringGetTextDatum(
+												 format_stats_values_as_text(&sslot));
+				nulls[10] = false;
+
+				free_attstatsslot(&sslot);
+			}
+		}
+
+		/* Correlation */
+		if (corr_slot >= 0)
+		{
+			AttStatsSlot sslot;
+
+			if (get_attstatsslot(&sslot, statstuple, STATISTIC_KIND_CORRELATION,
+								 InvalidOid, ATTSTATSSLOT_NUMBERS))
+			{
+				if (sslot.nnumbers > 0)
+				{
+					values[11] = Float4GetDatum(sslot.numbers[0]);
+					nulls[11] = false;
+				}
+
+				free_attstatsslot(&sslot);
+			}
+		}
+
+		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
+							 values, nulls);
+	}
+
+	return (Datum) 0;
+}
+
+/*
+ * pg_gtt_clear_stats
+ *		Discard per-session relation- and column-level statistics for a GTT.
+ *
+ * If a regclass argument is provided, clears stats only for that table.  If
+ * NULL (the default), clears stats for every GTT this session has touched.
+ * Affects only the calling session's private state; the planner falls back
+ * to default estimates until ANALYZE runs again in this session.
+ *
+ * Privilege rule mirrors the read-side SRFs (pg_gtt_relstats /
+ * pg_gtt_colstats): SELECT on the relation is sufficient.  A user can only
+ * affect stats they could already see, and the cleared state is private to
+ * the calling backend, so a stricter check would not buy anything.
+ */
+Datum
+pg_gtt_clear_stats(PG_FUNCTION_ARGS)
+{
+	HASH_SEQ_STATUS status;
+	GttStorageEntry *entry;
+	List	   *to_reset = NIL;
+	Oid			filter_relid = InvalidOid;
+
+	if (!PG_ARGISNULL(0))
+	{
+		filter_relid = PG_GETARG_OID(0);
+		if (OidIsValid(filter_relid))
+		{
+			if (pg_class_aclcheck(filter_relid, GetUserId(),
+								  ACL_SELECT) == ACLCHECK_OK)
+				GttResetSessionStats(filter_relid);
+		}
+		PG_RETURN_VOID();
+	}
+
+	if (gtt_storage_hash == NULL)
+		PG_RETURN_VOID();
+
+	/*
+	 * Collect the relids first.  GttResetSessionStats() calls
+	 * gtt_reset_colstats_for_rel() which walks the column-stats hash, and
+	 * mutating either hash inside an open hash_seq_search of the storage hash
+	 * is fragile; deferring keeps the iteration simple.
+	 */
+	hash_seq_init(&status, gtt_storage_hash);
+	while ((entry = (GttStorageEntry *) hash_seq_search(&status)) != NULL)
+	{
+		if (entry->is_index)
+			continue;
+		if (pg_class_aclcheck(entry->relid, GetUserId(),
+							  ACL_SELECT) != ACLCHECK_OK)
+			continue;
+		to_reset = lappend_oid(to_reset, entry->relid);
+	}
+
+	foreach_oid(relid, to_reset)
+		GttResetSessionStats(relid);
+	list_free(to_reset);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index c3c0a6e84ed..14e7bba3ba1 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -366,3 +366,41 @@ CREATE OR REPLACE FUNCTION ts_debug(document text,
 BEGIN ATOMIC
     SELECT * FROM ts_debug(get_current_ts_config(), $1);
 END;
+
+CREATE OR REPLACE FUNCTION pg_gtt_relstats(
+    relid regclass DEFAULT NULL,
+    OUT table_oid oid,
+    OUT table_name text,
+    OUT relpages int4,
+    OUT reltuples float4,
+    OUT relallvisible int4)
+ RETURNS SETOF record
+ LANGUAGE internal
+ VOLATILE CALLED ON NULL INPUT ROWS 10
+AS 'pg_gtt_relstats';
+
+CREATE OR REPLACE FUNCTION pg_gtt_colstats(
+    relid regclass DEFAULT NULL,
+    OUT table_oid oid,
+    OUT table_name text,
+    OUT attnum int2,
+    OUT attname text,
+    OUT inherited bool,
+    OUT null_frac float4,
+    OUT avg_width int4,
+    OUT n_distinct float4,
+    OUT most_common_vals text,
+    OUT most_common_freqs float4[],
+    OUT histogram_bounds text,
+    OUT correlation float4)
+ RETURNS SETOF record
+ LANGUAGE internal
+ VOLATILE CALLED ON NULL INPUT ROWS 10
+AS 'pg_gtt_colstats';
+
+CREATE OR REPLACE FUNCTION pg_gtt_clear_stats(
+    relid regclass DEFAULT NULL)
+ RETURNS void
+ LANGUAGE internal
+ VOLATILE CALLED ON NULL INPUT
+AS 'pg_gtt_clear_stats';
diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index f66e80b757c..397d3ad3850 100644
--- a/src/backend/commands/analyze.c
+++ b/src/backend/commands/analyze.c
@@ -28,7 +28,9 @@
 #include "access/xact.h"
 #include "catalog/index.h"
 #include "catalog/indexing.h"
+#include "catalog/pg_class.h"
 #include "catalog/pg_inherits.h"
+#include "catalog/storage_gtt.h"
 #include "commands/progress.h"
 #include "commands/tablecmds.h"
 #include "commands/vacuum.h"
@@ -94,7 +96,10 @@ static int	acquire_inherited_sample_rows(Relation onerel, int elevel,
 										  HeapTuple *rows, int targrows,
 										  double *totalrows, double *totaldeadrows);
 static void update_attstats(Oid relid, bool inh,
-							int natts, VacAttrStats **vacattrstats);
+							int natts, VacAttrStats **vacattrstats,
+							bool is_gtt);
+static HeapTuple build_statstuple(Oid relid, bool inh,
+								  VacAttrStats *stats, TupleDesc tupdesc);
 static Datum std_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull);
 static Datum ind_fetch_func(VacAttrStatsP stats, int rownum, bool *isNull);
 
@@ -316,7 +321,8 @@ do_analyze_rel(Relation onerel, const VacuumParams *params,
 	int			nindexes;
 	bool		verbose,
 				instrument,
-				hasindex;
+				hasindex,
+				is_gtt;
 	VacAttrStats **vacattrstats;
 	AnlIndexData *indexdata;
 	int			targrows,
@@ -340,6 +346,7 @@ do_analyze_rel(Relation onerel, const VacuumParams *params,
 	verbose = (params->options & VACOPT_VERBOSE) != 0;
 	instrument = (verbose || (AmAutoVacuumWorkerProcess() &&
 							  params->log_analyze_min_duration >= 0));
+	is_gtt = (RelationIsGlobalTemp(onerel));
 	if (inh)
 		ereport(elevel,
 				(errmsg("analyzing \"%s.%s\" inheritance tree",
@@ -608,21 +615,27 @@ do_analyze_rel(Relation onerel, const VacuumParams *params,
 		 * Emit the completed stats rows into pg_statistic, replacing any
 		 * previous statistics for the target columns.  (If there are stats in
 		 * pg_statistic for columns we didn't process, we leave them alone.)
+		 *
+		 * For global temporary tables, update_attstats stores the per-column
+		 * stats in backend-local memory instead of writing pg_statistic, so
+		 * each session sees its own sample.  Extended statistics are not
+		 * supported for GTTs and are skipped.
 		 */
 		update_attstats(RelationGetRelid(onerel), inh,
-						attr_cnt, vacattrstats);
+						attr_cnt, vacattrstats, is_gtt);
 
 		for (ind = 0; ind < nindexes; ind++)
 		{
 			AnlIndexData *thisdata = &indexdata[ind];
 
 			update_attstats(RelationGetRelid(Irel[ind]), false,
-							thisdata->attr_cnt, thisdata->vacattrstats);
+							thisdata->attr_cnt, thisdata->vacattrstats,
+							is_gtt);
 		}
 
-		/* Build extended statistics (if there are any). */
-		BuildRelationExtStatistics(onerel, inh, totalrows, numrows, rows,
-								   attr_cnt, vacattrstats);
+		if (!is_gtt)
+			BuildRelationExtStatistics(onerel, inh, totalrows, numrows, rows,
+									   attr_cnt, vacattrstats);
 	}
 
 	pgstat_progress_update_param(PROGRESS_ANALYZE_PHASE,
@@ -650,36 +663,52 @@ do_analyze_rel(Relation onerel, const VacuumParams *params,
 		/*
 		 * Update pg_class for table relation.  CCI first, in case acquirefunc
 		 * updated pg_class.
+		 *
+		 * For global temporary tables, store page/tuple statistics in the
+		 * per-session hash instead of pg_class: the shared catalog values
+		 * would be meaningless since each session has its own private data.
+		 * The planner reads them back via GttGetSessionStats().
 		 */
-		CommandCounterIncrement();
-		vac_update_relstats(onerel,
-							relpages,
-							totalrows,
-							relallvisible,
-							relallfrozen,
-							hasindex,
-							InvalidTransactionId,
-							InvalidMultiXactId,
-							NULL, NULL,
-							in_outer_xact);
-
-		/* Same for indexes */
-		for (ind = 0; ind < nindexes; ind++)
+		if (is_gtt)
+			GttUpdateSessionStats(RelationGetRelid(onerel),
+								  relpages, totalrows, relallvisible);
+		else
 		{
-			AnlIndexData *thisdata = &indexdata[ind];
-			double		totalindexrows;
-
-			totalindexrows = ceil(thisdata->tupleFract * totalrows);
-			vac_update_relstats(Irel[ind],
-								RelationGetNumberOfBlocks(Irel[ind]),
-								totalindexrows,
-								0, 0,
-								false,
+			CommandCounterIncrement();
+			vac_update_relstats(onerel,
+								relpages,
+								totalrows,
+								relallvisible,
+								relallfrozen,
+								hasindex,
 								InvalidTransactionId,
 								InvalidMultiXactId,
 								NULL, NULL,
 								in_outer_xact);
 		}
+
+		/* Same for indexes */
+		for (ind = 0; ind < nindexes; ind++)
+		{
+			AnlIndexData *thisdata = &indexdata[ind];
+			double		totalindexrows;
+
+			totalindexrows = ceil(thisdata->tupleFract * totalrows);
+			if (is_gtt)
+				GttUpdateSessionStats(RelationGetRelid(Irel[ind]),
+									  RelationGetNumberOfBlocks(Irel[ind]),
+									  totalindexrows, 0);
+			else
+				vac_update_relstats(Irel[ind],
+									RelationGetNumberOfBlocks(Irel[ind]),
+									totalindexrows,
+									0, 0,
+									false,
+									InvalidTransactionId,
+									InvalidMultiXactId,
+									NULL, NULL,
+									in_outer_xact);
+		}
 	}
 	else if (onerel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
 	{
@@ -1688,6 +1717,93 @@ acquire_inherited_sample_rows(Relation onerel, int elevel,
 }
 
 
+/*
+ * build_statstuple
+ *		Build a pg_statistic-format HeapTuple from a VacAttrStats.
+ *
+ * The tuple, and sizable intermediate arrays, are allocated in the current
+ * memory context.  A caller that needs the tuple to live beyond the current
+ * transaction should copy it (heap_copytuple) into the longer-lived context
+ * rather than build there, so the intermediates don't leak into it.
+ */
+static HeapTuple
+build_statstuple(Oid relid, bool inh, VacAttrStats *stats, TupleDesc tupdesc)
+{
+	int			i,
+				k,
+				n;
+	Datum		values[Natts_pg_statistic];
+	bool		nulls[Natts_pg_statistic];
+
+	for (i = 0; i < Natts_pg_statistic; ++i)
+		nulls[i] = false;
+
+	values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(relid);
+	values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(stats->tupattnum);
+	values[Anum_pg_statistic_stainherit - 1] = BoolGetDatum(inh);
+	values[Anum_pg_statistic_stanullfrac - 1] = Float4GetDatum(stats->stanullfrac);
+	values[Anum_pg_statistic_stawidth - 1] = Int32GetDatum(stats->stawidth);
+	values[Anum_pg_statistic_stadistinct - 1] = Float4GetDatum(stats->stadistinct);
+	i = Anum_pg_statistic_stakind1 - 1;
+	for (k = 0; k < STATISTIC_NUM_SLOTS; k++)
+	{
+		values[i++] = Int16GetDatum(stats->stakind[k]); /* stakindN */
+	}
+	i = Anum_pg_statistic_staop1 - 1;
+	for (k = 0; k < STATISTIC_NUM_SLOTS; k++)
+	{
+		values[i++] = ObjectIdGetDatum(stats->staop[k]);	/* staopN */
+	}
+	i = Anum_pg_statistic_stacoll1 - 1;
+	for (k = 0; k < STATISTIC_NUM_SLOTS; k++)
+	{
+		values[i++] = ObjectIdGetDatum(stats->stacoll[k]);	/* stacollN */
+	}
+	i = Anum_pg_statistic_stanumbers1 - 1;
+	for (k = 0; k < STATISTIC_NUM_SLOTS; k++)
+	{
+		if (stats->stanumbers[k] != NULL)
+		{
+			int			nnum = stats->numnumbers[k];
+			Datum	   *numdatums = (Datum *) palloc(nnum * sizeof(Datum));
+			ArrayType  *arry;
+
+			for (n = 0; n < nnum; n++)
+				numdatums[n] = Float4GetDatum(stats->stanumbers[k][n]);
+			arry = construct_array_builtin(numdatums, nnum, FLOAT4OID);
+			values[i++] = PointerGetDatum(arry);	/* stanumbersN */
+		}
+		else
+		{
+			nulls[i] = true;
+			values[i++] = (Datum) 0;
+		}
+	}
+	i = Anum_pg_statistic_stavalues1 - 1;
+	for (k = 0; k < STATISTIC_NUM_SLOTS; k++)
+	{
+		if (stats->stavalues[k] != NULL)
+		{
+			ArrayType  *arry;
+
+			arry = construct_array(stats->stavalues[k],
+								   stats->numvalues[k],
+								   stats->statypid[k],
+								   stats->statyplen[k],
+								   stats->statypbyval[k],
+								   stats->statypalign[k]);
+			values[i++] = PointerGetDatum(arry);	/* stavaluesN */
+		}
+		else
+		{
+			nulls[i] = true;
+			values[i++] = (Datum) 0;
+		}
+	}
+
+	return heap_form_tuple(tupdesc, values, nulls);
+}
+
 /*
  *	update_attstats() -- update attribute statistics for one relation
  *
@@ -1709,106 +1825,62 @@ acquire_inherited_sample_rows(Relation onerel, int elevel,
  *		Note: there would be a race condition here if two backends could
  *		ANALYZE the same table concurrently.  Presently, we lock that out
  *		by taking a self-exclusive lock on the relation in analyze_rel().
+ *
+ *		For global temporary tables (is_gtt == true) the caller wants
+ *		session-private statistics, so we build the tuples in
+ *		TopMemoryContext and store them in the backend-local GTT
+ *		column-stats hash instead of pg_statistic.  pg_statistic is opened
+ *		either way: to get a RowExclusiveLock for writing, or an
+ *		AccessShareLock for its TupleDesc.
  */
 static void
-update_attstats(Oid relid, bool inh, int natts, VacAttrStats **vacattrstats)
+update_attstats(Oid relid, bool inh, int natts, VacAttrStats **vacattrstats,
+				bool is_gtt)
 {
 	Relation	sd;
+	LOCKMODE	lockmode = is_gtt ? AccessShareLock : RowExclusiveLock;
 	int			attno;
 	CatalogIndexState indstate = NULL;
+	MemoryContext oldcxt;
 
 	if (natts <= 0)
 		return;					/* nothing to do */
 
-	sd = table_open(StatisticRelationId, RowExclusiveLock);
+	sd = table_open(StatisticRelationId, lockmode);
 
 	for (attno = 0; attno < natts; attno++)
 	{
 		VacAttrStats *stats = vacattrstats[attno];
 		HeapTuple	stup,
 					oldtup;
-		int			i,
-					k,
-					n;
-		Datum		values[Natts_pg_statistic];
-		bool		nulls[Natts_pg_statistic];
-		bool		replaces[Natts_pg_statistic];
 
 		/* Ignore attr if we weren't able to collect stats */
 		if (!stats->stats_valid)
 			continue;
 
-		/*
-		 * Construct a new pg_statistic tuple
-		 */
-		for (i = 0; i < Natts_pg_statistic; ++i)
+		if (is_gtt)
 		{
-			nulls[i] = false;
-			replaces[i] = true;
-		}
+			HeapTuple	stup_copy;
 
-		values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(relid);
-		values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(stats->tupattnum);
-		values[Anum_pg_statistic_stainherit - 1] = BoolGetDatum(inh);
-		values[Anum_pg_statistic_stanullfrac - 1] = Float4GetDatum(stats->stanullfrac);
-		values[Anum_pg_statistic_stawidth - 1] = Int32GetDatum(stats->stawidth);
-		values[Anum_pg_statistic_stadistinct - 1] = Float4GetDatum(stats->stadistinct);
-		i = Anum_pg_statistic_stakind1 - 1;
-		for (k = 0; k < STATISTIC_NUM_SLOTS; k++)
-		{
-			values[i++] = Int16GetDatum(stats->stakind[k]); /* stakindN */
-		}
-		i = Anum_pg_statistic_staop1 - 1;
-		for (k = 0; k < STATISTIC_NUM_SLOTS; k++)
-		{
-			values[i++] = ObjectIdGetDatum(stats->staop[k]);	/* staopN */
-		}
-		i = Anum_pg_statistic_stacoll1 - 1;
-		for (k = 0; k < STATISTIC_NUM_SLOTS; k++)
-		{
-			values[i++] = ObjectIdGetDatum(stats->stacoll[k]);	/* stacollN */
-		}
-		i = Anum_pg_statistic_stanumbers1 - 1;
-		for (k = 0; k < STATISTIC_NUM_SLOTS; k++)
-		{
-			if (stats->stanumbers[k] != NULL)
-			{
-				int			nnum = stats->numnumbers[k];
-				Datum	   *numdatums = (Datum *) palloc(nnum * sizeof(Datum));
-				ArrayType  *arry;
+			/*
+			 * GTT stats tuples outlive the current memory context: they are
+			 * owned by the backend-local hash in TopMemoryContext.  Build the
+			 * tuple in the current (transaction-lifetime) context, so the
+			 * sizable intermediate arrays build_statstuple creates are
+			 * reclaimed with it, and copy only the finished tuple into
+			 * TopMemoryContext.
+			 */
+			stup = build_statstuple(relid, inh, stats, RelationGetDescr(sd));
+			oldcxt = MemoryContextSwitchTo(TopMemoryContext);
+			stup_copy = heap_copytuple(stup);
+			MemoryContextSwitchTo(oldcxt);
+			heap_freetuple(stup);
 
-				for (n = 0; n < nnum; n++)
-					numdatums[n] = Float4GetDatum(stats->stanumbers[k][n]);
-				arry = construct_array_builtin(numdatums, nnum, FLOAT4OID);
-				values[i++] = PointerGetDatum(arry);	/* stanumbersN */
-			}
-			else
-			{
-				nulls[i] = true;
-				values[i++] = (Datum) 0;
-			}
+			GttStoreSessionColumnStats(relid, stats->tupattnum, inh, stup_copy);
+			continue;
 		}
-		i = Anum_pg_statistic_stavalues1 - 1;
-		for (k = 0; k < STATISTIC_NUM_SLOTS; k++)
-		{
-			if (stats->stavalues[k] != NULL)
-			{
-				ArrayType  *arry;
 
-				arry = construct_array(stats->stavalues[k],
-									   stats->numvalues[k],
-									   stats->statypid[k],
-									   stats->statyplen[k],
-									   stats->statypbyval[k],
-									   stats->statypalign[k]);
-				values[i++] = PointerGetDatum(arry);	/* stavaluesN */
-			}
-			else
-			{
-				nulls[i] = true;
-				values[i++] = (Datum) 0;
-			}
-		}
+		stup = build_statstuple(relid, inh, stats, RelationGetDescr(sd));
 
 		/* Is there already a pg_statistic tuple for this attribute? */
 		oldtup = SearchSysCache3(STATRELATTINH,
@@ -1822,19 +1894,23 @@ update_attstats(Oid relid, bool inh, int natts, VacAttrStats **vacattrstats)
 
 		if (HeapTupleIsValid(oldtup))
 		{
-			/* Yes, replace it */
-			stup = heap_modify_tuple(oldtup,
-									 RelationGetDescr(sd),
-									 values,
-									 nulls,
-									 replaces);
-			ReleaseSysCache(oldtup);
+			/*
+			 * Yes, replace it.  Preserve the old tuple's header fields
+			 * (t_self, t_ctid, t_tableOid) as heap_modify_tuple did before
+			 * build_statstuple was extracted; heap_update doesn't strictly
+			 * require all three, but keeping them consistent with the
+			 * previous behavior avoids surprises for any caller that inspects
+			 * them.
+			 */
+			stup->t_self = oldtup->t_self;
+			stup->t_data->t_ctid = oldtup->t_data->t_ctid;
+			stup->t_tableOid = oldtup->t_tableOid;
 			CatalogTupleUpdateWithInfo(sd, &stup->t_self, stup, indstate);
+			ReleaseSysCache(oldtup);
 		}
 		else
 		{
 			/* No, insert new tuple */
-			stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
 			CatalogTupleInsertWithInfo(sd, stup, indstate);
 		}
 
@@ -1843,7 +1919,7 @@ update_attstats(Oid relid, bool inh, int natts, VacAttrStats **vacattrstats)
 
 	if (indstate != NULL)
 		CatalogCloseIndexes(indstate);
-	table_close(sd, RowExclusiveLock);
+	table_close(sd, lockmode);
 }
 
 /*
diff --git a/src/backend/executor/nodeHash.c b/src/backend/executor/nodeHash.c
index 8825bb6fa23..4124f3155c5 100644
--- a/src/backend/executor/nodeHash.c
+++ b/src/backend/executor/nodeHash.c
@@ -29,6 +29,7 @@
 #include "access/htup_details.h"
 #include "access/parallel.h"
 #include "catalog/pg_statistic.h"
+#include "catalog/storage_gtt.h"
 #include "commands/tablespace.h"
 #include "executor/executor.h"
 #include "executor/hashjoin.h"
@@ -2430,6 +2431,7 @@ ExecHashBuildSkewHash(HashState *hashstate, HashJoinTable hashtable,
 					  Hash *node, int mcvsToUse)
 {
 	HeapTupleData *statsTuple;
+	void		(*freeStatsTuple) (HeapTuple);
 	AttStatsSlot sslot;
 
 	/* Do nothing if planner didn't identify the outer relation's join key */
@@ -2442,10 +2444,11 @@ ExecHashBuildSkewHash(HashState *hashstate, HashJoinTable hashtable,
 	/*
 	 * Try to find the MCV statistics for the outer relation's join key.
 	 */
-	statsTuple = SearchSysCache3(STATRELATTINH,
-								 ObjectIdGetDatum(node->skewTable),
-								 Int16GetDatum(node->skewColumn),
-								 BoolGetDatum(node->skewInherit));
+	statsTuple = SearchStats(node->skewTable,
+							 node->skewColumn,
+							 node->skewInherit,
+							 true,
+							 &freeStatsTuple);
 	if (!HeapTupleIsValid(statsTuple))
 		return;
 
@@ -2471,7 +2474,7 @@ ExecHashBuildSkewHash(HashState *hashstate, HashJoinTable hashtable,
 		if (frac < SKEW_MIN_OUTER_FRACTION)
 		{
 			free_attstatsslot(&sslot);
-			ReleaseSysCache(statsTuple);
+			freeStatsTuple(statsTuple);
 			return;
 		}
 
@@ -2567,7 +2570,7 @@ ExecHashBuildSkewHash(HashState *hashstate, HashJoinTable hashtable,
 		free_attstatsslot(&sslot);
 	}
 
-	ReleaseSysCache(statsTuple);
+	freeStatsTuple(statsTuple);
 }
 
 /*
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 7c4be174869..6f88ac6c6d2 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -28,7 +28,9 @@
 #include "catalog/catalog.h"
 #include "catalog/heap.h"
 #include "catalog/pg_am.h"
+#include "catalog/pg_class.h"
 #include "catalog/pg_proc.h"
+#include "catalog/storage_gtt.h"
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_statistic_ext_data.h"
 #include "foreign/fdwapi.h"
@@ -1311,6 +1313,64 @@ estimate_rel_size(Relation rel, int32 *attr_widths,
 	BlockNumber relallvisible;
 	double		density;
 
+	/*
+	 * For global temporary tables, use per-session statistics if available.
+	 * Each session has its own private GTT data, so the shared pg_class
+	 * statistics are not meaningful.  Per-session stats are populated by
+	 * ANALYZE and stored in the backend-local GTT storage hash.
+	 *
+	 * For both heap tables and indexes, we use the actual per-session page
+	 * count and the per-session tuple density from ANALYZE.  If ANALYZE
+	 * hasn't been run yet, we skip the "never vacuumed" minimum-10-pages
+	 * heuristic (which would overestimate, since GTTs genuinely start empty)
+	 * and fall through to the normal estimation paths.
+	 */
+	if (RelationIsGlobalTemp(rel))
+	{
+		BlockNumber sess_pages;
+		double		sess_tuples;
+		BlockNumber sess_allvisible;
+
+		if (GttGetSessionStats(RelationGetRelid(rel),
+							   &sess_pages, &sess_tuples, &sess_allvisible))
+		{
+			curpages = RelationGetNumberOfBlocks(rel);
+			*pages = curpages;
+
+			if (curpages == 0)
+			{
+				*tuples = 0;
+				*allvisfrac = 0;
+				return;
+			}
+
+			/* Use per-session tuple density to estimate current tuples */
+			if (sess_tuples >= 0 && sess_pages > 0)
+				density = sess_tuples / (double) sess_pages;
+			else
+			{
+				int32		tuple_width;
+
+				tuple_width = get_rel_data_width(rel, attr_widths);
+				tuple_width += MAXALIGN(SizeofHeapTupleHeader);
+				tuple_width += sizeof(ItemIdData);
+				density = (BLCKSZ - SizeOfPageHeaderData) / tuple_width;
+			}
+			*tuples = rint(density * (double) curpages);
+
+			if (sess_allvisible == 0 || curpages <= 0)
+				*allvisfrac = 0;
+			else if ((double) sess_allvisible >= curpages)
+				*allvisfrac = 1;
+			else
+				*allvisfrac = (double) sess_allvisible / curpages;
+
+			return;
+		}
+
+		/* No per-session stats yet; fall through to normal estimation. */
+	}
+
 	if (RELKIND_HAS_TABLE_AM(rel->rd_rel->relkind))
 	{
 		table_relation_estimate_size(rel, attr_widths, pages, tuples,
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index d6efd07073a..ba98454a628 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -107,6 +107,7 @@
 #include "catalog/pg_operator.h"
 #include "catalog/pg_statistic.h"
 #include "catalog/pg_statistic_ext.h"
+#include "catalog/storage_gtt.h"
 #include "executor/nodeAgg.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
@@ -5830,11 +5831,10 @@ examine_variable(PlannerInfo *root, Node *node, int varRelid,
 						else if (index->indpred == NIL)
 						{
 							vardata->statsTuple =
-								SearchSysCache3(STATRELATTINH,
-												ObjectIdGetDatum(index->indexoid),
-												Int16GetDatum(pos + 1),
-												BoolGetDatum(false));
-							vardata->freefunc = ReleaseSysCache;
+								SearchStats(index->indexoid,
+											pos + 1, false,
+											true,
+											&vardata->freefunc);
 
 							if (HeapTupleIsValid(vardata->statsTuple))
 							{
@@ -6060,11 +6060,11 @@ examine_simple_variable(PlannerInfo *root, Var *var,
 		 * Plain table or parent of an inheritance appendrel, so look up the
 		 * column in pg_statistic
 		 */
-		vardata->statsTuple = SearchSysCache3(STATRELATTINH,
-											  ObjectIdGetDatum(rte->relid),
-											  Int16GetDatum(var->varattno),
-											  BoolGetDatum(rte->inh));
-		vardata->freefunc = ReleaseSysCache;
+		vardata->statsTuple = SearchStats(rte->relid,
+										  var->varattno,
+										  rte->inh,
+										  true,
+										  &vardata->freefunc);
 
 		if (HeapTupleIsValid(vardata->statsTuple))
 		{
@@ -6537,11 +6537,10 @@ examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
 		}
 		else
 		{
-			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
-												  ObjectIdGetDatum(relid),
-												  Int16GetDatum(colnum),
-												  BoolGetDatum(rte->inh));
-			vardata->freefunc = ReleaseSysCache;
+			vardata->statsTuple = SearchStats(relid, colnum,
+											  rte->inh,
+											  true,
+											  &vardata->freefunc);
 		}
 	}
 	else
@@ -6563,11 +6562,9 @@ examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
 		}
 		else
 		{
-			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
-												  ObjectIdGetDatum(relid),
-												  Int16GetDatum(colnum),
-												  BoolGetDatum(false));
-			vardata->freefunc = ReleaseSysCache;
+			vardata->statsTuple = SearchStats(relid, colnum, false,
+											  true,
+											  &vardata->freefunc);
 		}
 	}
 }
@@ -9117,11 +9114,9 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 			else
 			{
 				vardata.statsTuple =
-					SearchSysCache3(STATRELATTINH,
-									ObjectIdGetDatum(rte->relid),
-									Int16GetDatum(attnum),
-									BoolGetDatum(false));
-				vardata.freefunc = ReleaseSysCache;
+					SearchStats(rte->relid, attnum, false,
+								true,
+								&vardata.freefunc);
 			}
 		}
 		else
@@ -9147,11 +9142,10 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 			}
 			else
 			{
-				vardata.statsTuple = SearchSysCache3(STATRELATTINH,
-													 ObjectIdGetDatum(index->indexoid),
-													 Int16GetDatum(attnum),
-													 BoolGetDatum(false));
-				vardata.freefunc = ReleaseSysCache;
+				vardata.statsTuple =
+					SearchStats(index->indexoid, attnum, false,
+								true,
+								&vardata.freefunc);
 			}
 		}
 
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 036de5f79ef..7a5249fed98 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -40,6 +40,7 @@
 #include "catalog/pg_range.h"
 #include "catalog/pg_statistic.h"
 #include "catalog/pg_subscription.h"
+#include "catalog/storage_gtt.h"
 #include "catalog/pg_transform.h"
 #include "catalog/pg_type.h"
 #include "miscadmin.h"
@@ -3467,6 +3468,7 @@ get_attavgwidth(Oid relid, AttrNumber attnum)
 {
 	HeapTuple	tp;
 	int32		stawidth;
+	void		(*freefunc) (HeapTuple);
 
 	if (get_attavgwidth_hook)
 	{
@@ -3474,17 +3476,17 @@ get_attavgwidth(Oid relid, AttrNumber attnum)
 		if (stawidth > 0)
 			return stawidth;
 	}
-	tp = SearchSysCache3(STATRELATTINH,
-						 ObjectIdGetDatum(relid),
-						 Int16GetDatum(attnum),
-						 BoolGetDatum(false));
+
+	tp = SearchStats(relid, attnum, false, true, &freefunc);
 	if (HeapTupleIsValid(tp))
 	{
 		stawidth = ((Form_pg_statistic) GETSTRUCT(tp))->stawidth;
-		ReleaseSysCache(tp);
+		freefunc	(tp);
+
 		if (stawidth > 0)
 			return stawidth;
 	}
+
 	return 0;
 }
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index be157a5fbe9..5d2f33de66f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12693,4 +12693,32 @@
   proname => 'hashoid8extended', prorettype => 'int8',
   proargtypes => 'oid8 int8', prosrc => 'hashoid8extended' },
 
+# Global temporary table per-session statistics
+# NB: These are redefined in system_functions.sql to add a DEFAULT NULL,
+# which can't be done here (regclass isn't in the bootstrap TypInfo, so
+# InsertOneProargdefaultsValue can't construct a Const for the default).
+{ oid => '9916',
+  descr => 'per-session relation statistics for global temporary tables',
+  proname => 'pg_gtt_relstats', prorows => '10', proretset => 't',
+  provolatile => 'v', proisstrict => 'f', prorettype => 'record',
+  proargtypes => 'regclass',
+  proallargtypes => '{regclass,oid,text,int4,float4,int4}',
+  proargmodes => '{i,o,o,o,o,o}',
+  proargnames => '{relid,table_oid,table_name,relpages,reltuples,relallvisible}',
+  prosrc => 'pg_gtt_relstats' },
+{ oid => '9917',
+  descr => 'per-session column statistics for global temporary tables',
+  proname => 'pg_gtt_colstats', prorows => '10', proretset => 't',
+  provolatile => 'v', proisstrict => 'f', prorettype => 'record',
+  proargtypes => 'regclass',
+  proallargtypes => '{regclass,oid,text,int2,text,bool,float4,int4,float4,text,_float4,text,float4}',
+  proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{relid,table_oid,table_name,attnum,attname,inherited,null_frac,avg_width,n_distinct,most_common_vals,most_common_freqs,histogram_bounds,correlation}',
+  prosrc => 'pg_gtt_colstats' },
+{ oid => '9918',
+  descr => 'discard per-session statistics for global temporary tables',
+  proname => 'pg_gtt_clear_stats', provolatile => 'v', proisstrict => 'f',
+  prorettype => 'void', proargtypes => 'regclass',
+  prosrc => 'pg_gtt_clear_stats' },
+
 ]
diff --git a/src/include/catalog/storage_gtt.h b/src/include/catalog/storage_gtt.h
index c2df6641600..92a4731eb31 100644
--- a/src/include/catalog/storage_gtt.h
+++ b/src/include/catalog/storage_gtt.h
@@ -28,4 +28,20 @@ extern void GttPrepareIndexAccess(Relation indexRelation);
 extern void PreCommit_gtt_on_commit(void);
 extern void GttResetAllSessionData(void);
 
+/* Per-session relation-level statistics for planner */
+extern bool GttGetSessionStats(Oid relid, BlockNumber *relpages,
+							   double *reltuples, BlockNumber *relallvisible);
+extern void GttUpdateSessionStats(Oid relid, BlockNumber relpages,
+								  double reltuples, BlockNumber relallvisible);
+extern void GttResetSessionStats(Oid relid);
+
+/* Per-session column-level statistics for planner */
+extern void GttStoreSessionColumnStats(Oid relid, AttrNumber attnum, bool inh,
+									   HeapTuple tuple);
+extern HeapTuple GttSearchColumnStats(Oid relid, AttrNumber attnum, bool inh);
+extern void GttReleaseColumnStats(HeapTuple tuple);
+extern HeapTuple SearchStats(Oid relid, AttrNumber attnum, bool inh,
+							 bool include_gtt,
+							 void (**freefunc) (HeapTuple));
+
 #endif							/* STORAGE_GTT_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 714a03dd3f3..d2b5c80aee5 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1187,6 +1187,8 @@ GroupingSet
 GroupingSetData
 GroupingSetKind
 GroupingSetsPath
+GttColStatsEntry
+GttColStatsKey
 GttStorageEntry
 GttSwapUndo
 GucAction
-- 
2.43.0

