From fe3d91ccf67dd8bcbccef48f70066163b2fcf0ba Mon Sep 17 00:00:00 2001
From: Dean Rasheed <dean.a.rasheed@gmail.com>
Date: Thu, 18 Jun 2026 20:49:45 +0100
Subject: [PATCH v10 08/11] Add pg_temp_statistic global temporary catalog
 table.

This has the exact same columns as pg_statistic, but it is a global
temporary table, and is used to hold statistics about other global
temporary relations, allowing them to be session-specific.
---
 doc/src/sgml/catalogs.sgml                  |  49 +++++++
 doc/src/sgml/indexam.sgml                   |   3 +-
 doc/src/sgml/perform.sgml                   |  18 ++-
 doc/src/sgml/planstats.sgml                 |  16 ++-
 doc/src/sgml/ref/analyze.sgml               |   7 +-
 doc/src/sgml/ref/explain.sgml               |  10 +-
 doc/src/sgml/system-views.sgml              |  16 ++-
 src/backend/catalog/genbki.pl               |   1 +
 src/backend/catalog/global_temp.c           |  29 ++++
 src/backend/catalog/heap.c                  |  22 ++-
 src/backend/catalog/system_views.sql        |   9 +-
 src/backend/commands/analyze.c              |  44 ++++--
 src/backend/executor/nodeHash.c             |   3 +-
 src/backend/statistics/attribute_stats.c    |  31 ++++-
 src/backend/utils/adt/selfuncs.c            |  18 ++-
 src/backend/utils/cache/lsyscache.c         |  12 +-
 src/include/catalog/Makefile                |   3 +-
 src/include/catalog/meson.build             |   1 +
 src/include/catalog/pg_statistic.h          |   3 +
 src/include/catalog/pg_temp_statistic.h     | 147 ++++++++++++++++++++
 src/test/isolation/expected/global-temp.out | 137 +++++++++++-------
 src/test/isolation/specs/global-temp.spec   |  14 +-
 src/test/regress/expected/global_temp.out   |  61 ++++++--
 src/test/regress/expected/oidjoins.out      |  11 ++
 src/test/regress/expected/rules.out         |  67 ++++++++-
 src/test/regress/expected/sanity_check.out  |  23 +++
 src/test/regress/sql/global_temp.sql        |  19 +++
 src/test/regress/sql/sanity_check.sql       |  20 +++
 28 files changed, 678 insertions(+), 116 deletions(-)
 create mode 100644 src/include/catalog/pg_temp_statistic.h

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index e616af12207..5ecaf4f2764 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -355,6 +355,11 @@
       <entry>global temporary relations used in the current session</entry>
      </row>
 
+     <row>
+      <entry><link linkend="catalog-pg-temp-statistic"><structname>pg_temp_statistic</structname></link></entry>
+      <entry>planner statistics for global temporary relations</entry>
+     </row>
+
      <row>
       <entry><link linkend="catalog-pg-transform"><structname>pg_transform</structname></link></entry>
       <entry>transforms (data type to procedural language conversions)</entry>
@@ -8173,6 +8178,16 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
    about those tables that are readable by the current user.
   </para>
 
+  <para>
+   Statistical data about global temporary relations is stored in
+   <link linkend="catalog-pg-temp-statistic"><structname>pg_temp_statistic</structname></link>,
+   instead of <structname>pg_statistic</structname>, because the contents of
+   global temporary relations can vary greatly between different sessions,
+   and so the statistical data needs to be local to each session
+   (<structname>pg_temp_statistic</structname> is itself a global temporary
+   table).
+  </para>
+
   <table>
    <title><structname>pg_statistic</structname> Columns</title>
    <tgroup cols="1">
@@ -9254,6 +9269,40 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
  </sect1>
 
 
+ <sect1 id="catalog-pg-temp-statistic">
+  <title><structname>pg_temp_statistic</structname></title>
+
+  <indexterm zone="catalog-pg-temp-statistic">
+   <primary>pg_temp_statistic</primary>
+  </indexterm>
+
+  <para>
+   The catalog <structname>pg_temp_statistic</structname> is a global
+   temporary table that stores statistical data about the contents of global
+   temporary relations in the database.  Entries are created by
+   <link linkend="sql-analyze"><command>ANALYZE</command></link>
+   and subsequently used by the query planner.
+  </para>
+
+  <para>
+   <structname>pg_temp_statistic</structname> has the same columns as
+   <structname>pg_statistic</structname>, and its contents are created and
+   used by the planner in exactly the same way, except that they are local to
+   the current session, and are automatically deleted when the session exits.
+   Therefore, each session should run
+   <link linkend="sql-analyze"><command>ANALYZE</command></link> to create
+   its own statistical data about the contents of global temporary relations
+   used by the session.
+  </para>
+
+  <para>
+   For more information about the table structure, and how its contents are
+   interpreted, see
+   <link linkend="catalog-pg-statistic"><structname>pg_statistic</structname></link>.
+  </para>
+ </sect1>
+
+
  <sect1 id="catalog-pg-transform">
   <title><structname>pg_transform</structname></title>
 
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index 3298717ace5..bc80be04569 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -1580,7 +1580,8 @@ cost_qual_eval(&amp;index_qual_cost, path-&gt;indexquals, root);
    <step>
     <para>
      Estimate the index correlation.  For a simple ordered index on a single
-     field, this can be retrieved from pg_statistic.  If the correlation
+     field, this can be retrieved from pg_statistic (or pg_temp_statistic,
+     for an index on a global temporary table).  If the correlation
      is not known, the conservative estimate is zero (no correlation).
     </para>
    </step>
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index 80a6509bb25..e0117a8d953 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -1327,6 +1327,7 @@ WHERE relname LIKE 'tenk1%';
 
   <indexterm>
    <primary>pg_statistic</primary>
+   <secondary>pg_temp_statistic</secondary>
   </indexterm>
 
   <para>
@@ -1338,7 +1339,10 @@ WHERE relname LIKE 'tenk1%';
    <literal>WHERE</literal> clause.  The information used for this task is
    stored in the
    <link linkend="catalog-pg-statistic"><structname>pg_statistic</structname></link>
-   system catalog.  Entries in <structname>pg_statistic</structname>
+   system catalog (or
+   <link linkend="catalog-pg-temp-statistic"><structname>pg_temp_statistic</structname></link>
+   for global temporary tables).  Entries in <structname>pg_statistic</structname>
+   and <structname>pg_temp_statistic</structname>
    are updated by the <command>ANALYZE</command> and <command>VACUUM
    ANALYZE</command> commands, and are always approximate even when freshly
    updated.
@@ -1349,13 +1353,15 @@ WHERE relname LIKE 'tenk1%';
   </indexterm>
 
   <para>
-   Rather than look at <structname>pg_statistic</structname> directly,
-   it's better to look at its view
+   Rather than look at <structname>pg_statistic</structname> and
+   <structname>pg_temp_statistic</structname> directly,
+   it's better to look at the view
    <link linkend="view-pg-stats"><structname>pg_stats</structname></link>
    when examining the statistics manually.  <structname>pg_stats</structname>
    is designed to be more easily readable.  Furthermore,
    <structname>pg_stats</structname> is readable by all, whereas
-   <structname>pg_statistic</structname> is only readable by a superuser.
+   <structname>pg_statistic</structname> and
+   <structname>pg_temp_statistic</structname> are only readable by a superuser.
    (This prevents unprivileged users from learning something about
    the contents of other people's tables from the statistics.  The
    <structname>pg_stats</structname> view is restricted to show only
@@ -1408,6 +1414,7 @@ WHERE tablename = 'road';
 
   <para>
    The amount of information stored in <structname>pg_statistic</structname>
+   and <structname>pg_temp_statistic</structname>
    by <command>ANALYZE</command>, in particular the maximum number of entries in the
    <structfield>most_common_vals</structfield> and <structfield>histogram_bounds</structfield>
    arrays for each column, can be set on a
@@ -1417,7 +1424,8 @@ WHERE tablename = 'road';
    The default limit is presently 100 entries.  Raising the limit
    might allow more accurate planner estimates to be made, particularly for
    columns with irregular data distributions, at the price of consuming
-   more space in <structname>pg_statistic</structname> and slightly more
+   more space in <structname>pg_statistic</structname> and
+   <structname>pg_temp_statistic</structname>, and slightly more
    time to compute the estimates.  Conversely, a lower limit might be
    sufficient for columns with simple data distributions.
   </para>
diff --git a/doc/src/sgml/planstats.sgml b/doc/src/sgml/planstats.sgml
index 72f242f2cb3..3fba3a584cb 100644
--- a/doc/src/sgml/planstats.sgml
+++ b/doc/src/sgml/planstats.sgml
@@ -93,7 +93,9 @@ EXPLAIN SELECT * FROM tenk1 WHERE unique1 &lt; 1000;
    and the entry in this case is <function>scalarltsel</function>.
    The <function>scalarltsel</function> function retrieves the histogram for
    <structfield>unique1</structfield> from
-   <structname>pg_statistic</structname>.  For manual queries it is more
+   <structname>pg_statistic</structname> (or
+   <structname>pg_temp_statistic</structname> for a global temporary
+   relation).  For manual queries it is more
    convenient to look in the simpler <structname>pg_stats</structname>
    view:
 
@@ -705,7 +707,8 @@ EXPLAIN (ANALYZE, TIMING OFF, BUFFERS OFF) SELECT * FROM t WHERE a &lt;= 49 AND
   <title>Planner Statistics and Security</title>
 
   <para>
-   Access to the table <structname>pg_statistic</structname> is restricted to
+   Access to the tables <structname>pg_statistic</structname> and
+   <structname>pg_temp_statistic</structname> is restricted to
    superusers, so that ordinary users cannot learn about the contents of the
    tables of other users from it.  Some selectivity estimation functions will
    use a user-provided operator (either the operator appearing in the query or
@@ -713,12 +716,14 @@ EXPLAIN (ANALYZE, TIMING OFF, BUFFERS OFF) SELECT * FROM t WHERE a &lt;= 49 AND
    to determine whether a stored most common value is applicable, the
    selectivity estimator will have to run the appropriate <literal>=</literal>
    operator to compare the constant in the query to the stored value.
-   Thus the data in <structname>pg_statistic</structname> is potentially
+   Thus the data in <structname>pg_statistic</structname> and
+   <structname>pg_temp_statistic</structname> is potentially
    passed to user-defined operators.  An appropriately crafted operator can
    intentionally leak the passed operands (for example, by logging them
    or writing them to a different table), or accidentally leak them by showing
    their values in error messages, in either case possibly exposing data from
-   <structname>pg_statistic</structname> to a user who should not be able to
+   <structname>pg_statistic</structname> and
+   <structname>pg_temp_statistic</structname> to a user who should not be able to
    see it.
   </para>
 
@@ -752,7 +757,8 @@ EXPLAIN (ANALYZE, TIMING OFF, BUFFERS OFF) SELECT * FROM t WHERE a &lt;= 49 AND
   <para>
    This restriction applies only to cases where the planner would need to
    execute a user-defined operator on one or more values
-   from <structname>pg_statistic</structname>.  Thus the planner is permitted
+   from <structname>pg_statistic</structname> or
+   <structname>pg_temp_statistic</structname>.  Thus the planner is permitted
    to use generic statistical information, such as the fraction of null values
    or the number of distinct values in a column, regardless of access
    privileges.
diff --git a/doc/src/sgml/ref/analyze.sgml b/doc/src/sgml/ref/analyze.sgml
index d27c0d9e864..20bf00d07d3 100644
--- a/doc/src/sgml/ref/analyze.sgml
+++ b/doc/src/sgml/ref/analyze.sgml
@@ -42,7 +42,9 @@ ANALYZE [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] [ <r
    <command>ANALYZE</command> collects statistics about the contents
    of tables in the database, and stores the results in the <link
    linkend="catalog-pg-statistic"><structname>pg_statistic</structname></link>
-   system catalog.  Subsequently, the query planner uses these
+   system catalog (and in <link
+   linkend="catalog-pg-temp-statistic"><structname>pg_temp_statistic</structname></link>
+   for global temporary tables).  Subsequently, the query planner uses these
    statistics to help determine the most efficient execution plans for
    queries.
   </para>
@@ -259,7 +261,8 @@ ANALYZE [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] [ <r
    is 100, but this can be adjusted up or down to trade off accuracy of
    planner estimates against the time taken for
    <command>ANALYZE</command> and the amount of space occupied in
-   <literal>pg_statistic</literal>.  In particular, setting the
+   <literal>pg_statistic</literal> and <literal>pg_temp_statistic</literal>.
+   In particular, setting the
    statistics target to zero disables collection of statistics for
    that column.  It might be useful to do that for columns that are
    never used as part of the <literal>WHERE</literal>, <literal>GROUP BY</literal>,
diff --git a/doc/src/sgml/ref/explain.sgml b/doc/src/sgml/ref/explain.sgml
index f38d31e106a..caf8ee7664b 100644
--- a/doc/src/sgml/ref/explain.sgml
+++ b/doc/src/sgml/ref/explain.sgml
@@ -378,12 +378,18 @@ ROLLBACK;
    planner to make reasonably informed decisions when optimizing
    queries, the <link
    linkend="catalog-pg-statistic"><structname>pg_statistic</structname></link>
-   data should be up-to-date for all tables used in the query.  Normally
+   data should be up-to-date for all tables used in the query (and the
+   <link linkend="catalog-pg-temp-statistic"><structname>pg_temp_statistic</structname></link>
+   data for any global temporary tables used).  Normally
    the <link linkend="autovacuum">autovacuum daemon</link> will take care
    of that automatically.  But if a table has recently had substantial
    changes in its contents, you might need to do a manual
    <link linkend="sql-analyze"><command>ANALYZE</command></link> rather than wait for autovacuum to catch up
-   with the changes.
+   with the changes.  Note also that the autovacuum daemon cannot analyze
+   temporary tables, because the data in temporary tables is only visible to
+   the backend that inserted it, so when querying from temporary tables, it
+   may be necessary to run <command>ANALYZE</command> manually from the same
+   session.
   </para>
 
   <para>
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 77202e2c765..6b1dd82e044 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -4367,17 +4367,21 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
    The view <structname>pg_stats</structname> provides access to
    the information stored in the <link
    linkend="catalog-pg-statistic"><structname>pg_statistic</structname></link>
-   catalog.  This view allows access only to rows of
-   <link linkend="catalog-pg-statistic"><structname>pg_statistic</structname></link> that correspond to tables the
-   user has permission to read, and therefore it is safe to allow public
-   read access to this view.
+   and <link
+   linkend="catalog-pg-temp-statistic"><structname>pg_temp_statistic</structname></link>
+   catalogs.  This view allows access only to rows of
+   <link linkend="catalog-pg-statistic"><structname>pg_statistic</structname></link> and
+   <link linkend="catalog-pg-temp-statistic"><structname>pg_temp_statistic</structname></link>
+   that correspond to tables the user has permission to read, and therefore
+   it is safe to allow public read access to this view.
   </para>
 
   <para>
    <structname>pg_stats</structname> is also designed to present the
-   information in a more readable format than the underlying catalog
+   information in a more readable format than the underlying catalogs
    &mdash; at the cost that its schema must be extended whenever new slot types
-   are defined for <link linkend="catalog-pg-statistic"><structname>pg_statistic</structname></link>.
+   are defined for <link linkend="catalog-pg-statistic"><structname>pg_statistic</structname></link>
+   and <link linkend="catalog-pg-temp-statistic"><structname>pg_temp_statistic</structname></link>.
   </para>
 
   <table>
diff --git a/src/backend/catalog/genbki.pl b/src/backend/catalog/genbki.pl
index 7623760912e..fe58334747d 100644
--- a/src/backend/catalog/genbki.pl
+++ b/src/backend/catalog/genbki.pl
@@ -801,6 +801,7 @@ print $syscache_ids_fh "#ifndef SYSCACHE_IDS_H
 #define SYSCACHE_IDS_H
 
 #include \"catalog/pg_temp_class_d.h\"
+#include \"catalog/pg_temp_statistic_d.h\"
 
 typedef enum SysCacheIdentifier
 {
diff --git a/src/backend/catalog/global_temp.c b/src/backend/catalog/global_temp.c
index a300b37ac37..7d265ab76a2 100644
--- a/src/backend/catalog/global_temp.c
+++ b/src/backend/catalog/global_temp.c
@@ -60,6 +60,7 @@
 #include "access/xact.h"
 #include "access/xlogutils.h"
 #include "catalog/global_temp.h"
+#include "catalog/indexing.h"
 #include "catalog/pg_temp_class.h"
 #include "catalog/storage.h"
 #include "commands/sequence.h"
@@ -71,6 +72,7 @@
 #include "storage/proc.h"
 #include "storage/shmem.h"
 #include "storage/subsystems.h"
+#include "utils/fmgroids.h"
 #include "utils/gtcatcache.h"
 #include "utils/memutils.h"
 #include "utils/syscache.h"
@@ -1143,6 +1145,7 @@ ProcessInvalidatedGlobalTempRelations(void)
 	if (gtrs_dropped && processed_dropped_subid == InvalidSubTransactionId)
 	{
 		bool		tuples_deleted = false;
+		Relation	statrel;
 
 		/*
 		 * Delete and forget locally-created storage for dropped relations.
@@ -1180,8 +1183,14 @@ ProcessInvalidatedGlobalTempRelations(void)
 		 * record removal is non-transactional, but the rest may be undone by
 		 * (sub)rollback.
 		 */
+		statrel = table_open(TempStatisticRelationId, RowExclusiveLock);
+
 		foreach_oid(relid, gtrs_dropped)
 		{
+			ScanKeyData key[1];
+			SysScanDesc scan;
+			HeapTuple	tuple;
+
 			gtr_remove_usage(relid);
 			remove_on_commit_action(relid);
 
@@ -1191,8 +1200,28 @@ ProcessInvalidatedGlobalTempRelations(void)
 				DeletePgTempClassTuple(relid);
 				tuples_deleted = true;
 			}
+
+			/* Delete any per-column statistics from pg_temp_statistic */
+			ScanKeyInit(&key[0],
+						Anum_pg_temp_statistic_starelid,
+						BTEqualStrategyNumber, F_OIDEQ,
+						ObjectIdGetDatum(relid));
+
+			scan = systable_beginscan(statrel,
+									  TempStatisticRelidAttnumInhIndexId,
+									  true, NULL, 1, key);
+
+			while (HeapTupleIsValid(tuple = systable_getnext(scan)))
+			{
+				CatalogTupleDelete(statrel, &tuple->t_self);
+				tuples_deleted = true;
+			}
+
+			systable_endscan(scan);
 		}
 
+		table_close(statrel, RowExclusiveLock);
+
 		/* If we deleted anything, make the changes visible */
 		if (tuples_deleted)
 			CommandCounterIncrement();
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 97ea51382c3..2d24b506e4e 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -53,6 +53,7 @@
 #include "catalog/pg_statistic.h"
 #include "catalog/pg_subscription_rel.h"
 #include "catalog/pg_tablespace.h"
+#include "catalog/pg_temp_statistic.h"
 #include "catalog/pg_type.h"
 #include "catalog/storage.h"
 #include "commands/tablecmds.h"
@@ -3515,6 +3516,13 @@ CopyStatistics(Oid fromrelid, Oid torelid)
 	Relation	statrel;
 	CatalogIndexState indstate = NULL;
 
+	/*
+	 * Note: This is currently only used for concurrent index building, which
+	 * isn't supported on global temporary relations, so we never want
+	 * pg_temp_statistic here.
+	 */
+	Assert(!rel_is_global_temp(fromrelid) && !rel_is_global_temp(torelid));
+
 	statrel = table_open(StatisticRelationId, RowExclusiveLock);
 
 	/* Now search for stat records */
@@ -3563,12 +3571,22 @@ void
 RemoveStatistics(Oid relid, AttrNumber attnum)
 {
 	Relation	pgstatistic;
+	Oid			relidAttnumInhIndexId;
 	SysScanDesc scan;
 	ScanKeyData key[2];
 	int			nkeys;
 	HeapTuple	tuple;
 
-	pgstatistic = table_open(StatisticRelationId, RowExclusiveLock);
+	if (rel_is_global_temp(relid))
+	{
+		pgstatistic = table_open(TempStatisticRelationId, RowExclusiveLock);
+		relidAttnumInhIndexId = TempStatisticRelidAttnumInhIndexId;
+	}
+	else
+	{
+		pgstatistic = table_open(StatisticRelationId, RowExclusiveLock);
+		relidAttnumInhIndexId = StatisticRelidAttnumInhIndexId;
+	}
 
 	ScanKeyInit(&key[0],
 				Anum_pg_statistic_starelid,
@@ -3586,7 +3604,7 @@ RemoveStatistics(Oid relid, AttrNumber attnum)
 		nkeys = 2;
 	}
 
-	scan = systable_beginscan(pgstatistic, StatisticRelidAttnumInhIndexId, true,
+	scan = systable_beginscan(pgstatistic, relidAttnumInhIndexId, true,
 							  NULL, nkeys, key);
 
 	/* we must loop even when attnum != 0, in case of inherited stats */
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 04e47a600b7..8e1369caa9c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -187,6 +187,11 @@ CREATE VIEW pg_sequences AS
     WHERE NOT pg_is_other_temp_schema(N.oid)
           AND relkind = 'S';
 
+CREATE VIEW pg_all_statistic AS
+    SELECT * FROM pg_statistic
+    UNION ALL
+    SELECT * FROM pg_temp_statistic;
+
 CREATE VIEW pg_stats WITH (security_barrier) AS
     SELECT
         nspname AS schemaname,
@@ -268,7 +273,7 @@ CREATE VIEW pg_stats WITH (security_barrier) AS
             WHEN stakind4 = 7 THEN stavalues4
             WHEN stakind5 = 7 THEN stavalues5
             END AS range_bounds_histogram
-    FROM pg_statistic s JOIN pg_class c ON (c.oid = s.starelid)
+    FROM pg_all_statistic s JOIN pg_class c ON (c.oid = s.starelid)
          JOIN pg_attribute a ON (c.oid = attrelid AND attnum = s.staattnum)
          LEFT JOIN pg_namespace n ON (n.oid = c.relnamespace)
     WHERE NOT attisdropped
@@ -276,6 +281,8 @@ CREATE VIEW pg_stats WITH (security_barrier) AS
     AND (c.relrowsecurity = false OR NOT row_security_active(c.oid));
 
 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_stats_ext WITH (security_barrier) AS
     SELECT cn.nspname AS schemaname,
diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index c05f9f50e43..2434e5edca4 100644
--- a/src/backend/commands/analyze.c
+++ b/src/backend/commands/analyze.c
@@ -29,6 +29,7 @@
 #include "catalog/index.h"
 #include "catalog/indexing.h"
 #include "catalog/pg_inherits.h"
+#include "catalog/pg_temp_statistic.h"
 #include "commands/progress.h"
 #include "commands/tablecmds.h"
 #include "commands/vacuum.h"
@@ -177,9 +178,11 @@ analyze_rel(Oid relid, RangeVar *relation,
 	}
 
 	/*
-	 * We can ANALYZE any table except pg_statistic. See update_attstats
+	 * We can ANALYZE any table except pg_statistic and pg_temp_statistic. See
+	 * update_attstats
 	 */
-	if (RelationGetRelid(onerel) == StatisticRelationId)
+	if (RelationGetRelid(onerel) == StatisticRelationId ||
+		RelationGetRelid(onerel) == TempStatisticRelationId)
 	{
 		relation_close(onerel, ShareUpdateExclusiveLock);
 		return;
@@ -1694,20 +1697,23 @@ acquire_inherited_sample_rows(Relation onerel, int elevel,
 /*
  *	update_attstats() -- update attribute statistics for one relation
  *
- *		Statistics are stored in several places: the pg_class row for the
- *		relation has stats about the whole relation, and there is a
- *		pg_statistic row for each (non-system) attribute that has ever
- *		been analyzed.  The pg_class values are updated by VACUUM, not here.
+ *		Statistics are stored in several places: the pg_class/pg_temp_class
+ *		row for the relation has stats about the whole relation, and there is
+ *		a pg_statistic/pg_temp_statistic row for each (non-system) attribute
+ *		that has ever been analyzed.  The pg_class/pg_temp_class values are
+ *		updated by VACUUM, not here.
  *
- *		pg_statistic rows are just added or updated normally.  This means
- *		that pg_statistic will probably contain some deleted rows at the
- *		completion of a vacuum cycle, unless it happens to get vacuumed last.
+ *		pg_statistic/pg_temp_statistic rows are just added or updated
+ *		normally.  This means that pg_statistic/pg_temp_statistic will
+ *		probably contain some deleted rows at the completion of a vacuum
+ *		cycle, unless it happens to get vacuumed last.
  *
- *		To keep things simple, we punt for pg_statistic, and don't try
- *		to compute or store rows for pg_statistic itself in pg_statistic.
+ *		To keep things simple, we punt for pg_statistic and pg_temp_statistic,
+ *		and don't try to compute or store rows for pg_statistic or
+ *		pg_temp_statistic themselves in pg_statistic or pg_temp_statistic.
  *		This could possibly be made to work, but it's not worth the trouble.
  *		Note analyze_rel() has seen to it that we won't come here when
- *		vacuuming pg_statistic itself.
+ *		vacuuming pg_statistic or pg_temp_statistic themselves.
  *
  *		Note: there would be a race condition here if two backends could
  *		ANALYZE the same table concurrently.  Presently, we lock that out
@@ -1719,11 +1725,21 @@ update_attstats(Oid relid, bool inh, int natts, VacAttrStats **vacattrstats)
 	Relation	sd;
 	int			attno;
 	CatalogIndexState indstate = NULL;
+	SysCacheIdentifier cacheId;
 
 	if (natts <= 0)
 		return;					/* nothing to do */
 
-	sd = table_open(StatisticRelationId, RowExclusiveLock);
+	if (rel_is_global_temp(relid))
+	{
+		sd = table_open(TempStatisticRelationId, RowExclusiveLock);
+		cacheId = TEMPSTATRELATTINH;
+	}
+	else
+	{
+		sd = table_open(StatisticRelationId, RowExclusiveLock);
+		cacheId = STATRELATTINH;
+	}
 
 	for (attno = 0; attno < natts; attno++)
 	{
@@ -1814,7 +1830,7 @@ update_attstats(Oid relid, bool inh, int natts, VacAttrStats **vacattrstats)
 		}
 
 		/* Is there already a pg_statistic tuple for this attribute? */
-		oldtup = SearchSysCache3(STATRELATTINH,
+		oldtup = SearchSysCache3(cacheId,
 								 ObjectIdGetDatum(relid),
 								 Int16GetDatum(stats->tupattnum),
 								 BoolGetDatum(inh));
diff --git a/src/backend/executor/nodeHash.c b/src/backend/executor/nodeHash.c
index 8825bb6fa23..6ace6904810 100644
--- a/src/backend/executor/nodeHash.c
+++ b/src/backend/executor/nodeHash.c
@@ -2442,7 +2442,8 @@ ExecHashBuildSkewHash(HashState *hashstate, HashJoinTable hashtable,
 	/*
 	 * Try to find the MCV statistics for the outer relation's join key.
 	 */
-	statsTuple = SearchSysCache3(STATRELATTINH,
+	statsTuple = SearchSysCache3(rel_is_global_temp(node->skewTable) ?
+								 TEMPSTATRELATTINH : STATRELATTINH,
 								 ObjectIdGetDatum(node->skewTable),
 								 Int16GetDatum(node->skewColumn),
 								 BoolGetDatum(node->skewInherit));
diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index 1d19827dc45..562f5da4a74 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -21,6 +21,7 @@
 #include "catalog/indexing.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_operator.h"
+#include "catalog/pg_temp_statistic.h"
 #include "nodes/makefuncs.h"
 #include "statistics/statistics.h"
 #include "statistics/stat_utils.h"
@@ -236,6 +237,7 @@ attribute_statistics_update_internal(Oid reloid,
 									 const AttributeStatsValues *statvalues)
 {
 	Relation	starel;
+	SysCacheIdentifier cacheId;
 	HeapTuple	statup;
 
 	Oid			atttypid = InvalidOid;
@@ -372,9 +374,18 @@ attribute_statistics_update_internal(Oid reloid,
 
 	fmgr_info(F_ARRAY_IN, &array_in_fn);
 
-	starel = table_open(StatisticRelationId, RowExclusiveLock);
+	if (rel_is_global_temp(reloid))
+	{
+		starel = table_open(TempStatisticRelationId, RowExclusiveLock);
+		cacheId = TEMPSTATRELATTINH;
+	}
+	else
+	{
+		starel = table_open(StatisticRelationId, RowExclusiveLock);
+		cacheId = STATRELATTINH;
+	}
 
-	statup = SearchSysCache3(STATRELATTINH, ObjectIdGetDatum(reloid), Int16GetDatum(attnum), BoolGetDatum(inherited));
+	statup = SearchSysCache3(cacheId, ObjectIdGetDatum(reloid), Int16GetDatum(attnum), BoolGetDatum(inherited));
 
 	/* initialize from existing tuple if exists */
 	if (HeapTupleIsValid(statup))
@@ -616,12 +627,24 @@ upsert_pg_statistic(Relation starel, HeapTuple oldtup,
 static bool
 delete_pg_statistic(Oid reloid, AttrNumber attnum, bool stainherit)
 {
-	Relation	sd = table_open(StatisticRelationId, RowExclusiveLock);
+	Relation	sd;
 	HeapTuple	oldtup;
 	bool		result = false;
+	SysCacheIdentifier cacheId;
+
+	if (rel_is_global_temp(reloid))
+	{
+		sd = table_open(TempStatisticRelationId, RowExclusiveLock);
+		cacheId = TEMPSTATRELATTINH;
+	}
+	else
+	{
+		sd = table_open(StatisticRelationId, RowExclusiveLock);
+		cacheId = STATRELATTINH;
+	}
 
 	/* Is there already a pg_statistic tuple for this attribute? */
-	oldtup = SearchSysCache3(STATRELATTINH,
+	oldtup = SearchSysCache3(cacheId,
 							 ObjectIdGetDatum(reloid),
 							 Int16GetDatum(attnum),
 							 BoolGetDatum(stainherit));
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index e27ec9e5c25..1db37761d9b 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -5839,7 +5839,8 @@ examine_variable(PlannerInfo *root, Node *node, int varRelid,
 						else if (index->indpred == NIL)
 						{
 							vardata->statsTuple =
-								SearchSysCache3(STATRELATTINH,
+								SearchSysCache3(rel_is_global_temp(index->indexoid) ?
+												TEMPSTATRELATTINH : STATRELATTINH,
 												ObjectIdGetDatum(index->indexoid),
 												Int16GetDatum(pos + 1),
 												BoolGetDatum(false));
@@ -6069,7 +6070,8 @@ 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,
+		vardata->statsTuple = SearchSysCache3(rel_is_global_temp(rte->relid) ?
+											  TEMPSTATRELATTINH : STATRELATTINH,
 											  ObjectIdGetDatum(rte->relid),
 											  Int16GetDatum(var->varattno),
 											  BoolGetDatum(rte->inh));
@@ -6638,7 +6640,8 @@ examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
 		}
 		else
 		{
-			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+			vardata->statsTuple = SearchSysCache3(rel_is_global_temp(relid) ?
+												  TEMPSTATRELATTINH : STATRELATTINH,
 												  ObjectIdGetDatum(relid),
 												  Int16GetDatum(colnum),
 												  BoolGetDatum(rte->inh));
@@ -6664,7 +6667,8 @@ examine_indexcol_variable(PlannerInfo *root, IndexOptInfo *index,
 		}
 		else
 		{
-			vardata->statsTuple = SearchSysCache3(STATRELATTINH,
+			vardata->statsTuple = SearchSysCache3(rel_is_global_temp(relid) ?
+												  TEMPSTATRELATTINH : STATRELATTINH,
 												  ObjectIdGetDatum(relid),
 												  Int16GetDatum(colnum),
 												  BoolGetDatum(false));
@@ -9218,7 +9222,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 			else
 			{
 				vardata.statsTuple =
-					SearchSysCache3(STATRELATTINH,
+					SearchSysCache3(rel_is_global_temp(rte->relid) ?
+									TEMPSTATRELATTINH : STATRELATTINH,
 									ObjectIdGetDatum(rte->relid),
 									Int16GetDatum(attnum),
 									BoolGetDatum(false));
@@ -9248,7 +9253,8 @@ brincostestimate(PlannerInfo *root, IndexPath *path, double loop_count,
 			}
 			else
 			{
-				vardata.statsTuple = SearchSysCache3(STATRELATTINH,
+				vardata.statsTuple = SearchSysCache3(rel_is_global_temp(index->indexoid) ?
+													 TEMPSTATRELATTINH : STATRELATTINH,
 													 ObjectIdGetDatum(index->indexoid),
 													 Int16GetDatum(attnum),
 													 BoolGetDatum(false));
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 1156f5f5eaa..dd7b04b5e3d 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -41,6 +41,7 @@
 #include "catalog/pg_statistic.h"
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_temp_class.h"
+#include "catalog/pg_temp_statistic.h"
 #include "catalog/pg_transform.h"
 #include "catalog/pg_type.h"
 #include "miscadmin.h"
@@ -3507,7 +3508,8 @@ get_attavgwidth(Oid relid, AttrNumber attnum)
 		if (stawidth > 0)
 			return stawidth;
 	}
-	tp = SearchSysCache3(STATRELATTINH,
+	tp = SearchSysCache3(rel_is_global_temp(relid) ?
+						 TEMPSTATRELATTINH : STATRELATTINH,
 						 ObjectIdGetDatum(relid),
 						 Int16GetDatum(attnum),
 						 BoolGetDatum(false));
@@ -3601,7 +3603,9 @@ get_attstatsslot(AttStatsSlot *sslot, HeapTuple statstuple,
 
 	if (flags & ATTSTATSSLOT_VALUES)
 	{
-		val = SysCacheGetAttrNotNull(STATRELATTINH, statstuple,
+		val = SysCacheGetAttrNotNull(IsTempStatisticTuple(statstuple) ?
+									 TEMPSTATRELATTINH : STATRELATTINH,
+									 statstuple,
 									 Anum_pg_statistic_stavalues1 + i);
 
 		/*
@@ -3646,7 +3650,9 @@ get_attstatsslot(AttStatsSlot *sslot, HeapTuple statstuple,
 
 	if (flags & ATTSTATSSLOT_NUMBERS)
 	{
-		val = SysCacheGetAttrNotNull(STATRELATTINH, statstuple,
+		val = SysCacheGetAttrNotNull(IsTempStatisticTuple(statstuple) ?
+									 TEMPSTATRELATTINH : STATRELATTINH,
+									 statstuple,
 									 Anum_pg_statistic_stanumbers1 + i);
 
 		/*
diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile
index 629a13edc24..6aec03add22 100644
--- a/src/include/catalog/Makefile
+++ b/src/include/catalog/Makefile
@@ -87,7 +87,8 @@ CATALOG_HEADERS := \
 	pg_propgraph_label.h \
 	pg_propgraph_label_property.h \
 	pg_propgraph_property.h \
-	pg_temp_class.h
+	pg_temp_class.h \
+	pg_temp_statistic.h
 
 GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h)
 
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index 404f8503276..db809c4b3c0 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -75,6 +75,7 @@ catalog_headers = [
   'pg_propgraph_label_property.h',
   'pg_propgraph_property.h',
   'pg_temp_class.h',
+  'pg_temp_statistic.h',
 ]
 
 # The .dat files we need can just be listed alphabetically.
diff --git a/src/include/catalog/pg_statistic.h b/src/include/catalog/pg_statistic.h
index 032bf177b95..ff55f7f5bb4 100644
--- a/src/include/catalog/pg_statistic.h
+++ b/src/include/catalog/pg_statistic.h
@@ -28,6 +28,9 @@
  */
 BEGIN_CATALOG_STRUCT
 
+/*
+ * NB: Any changes made here must be reflected in pg_temp_statistic.
+ */
 CATALOG(pg_statistic,2619,StatisticRelationId)
 {
 	/* These fields form the unique key for the entry: */
diff --git a/src/include/catalog/pg_temp_statistic.h b/src/include/catalog/pg_temp_statistic.h
new file mode 100644
index 00000000000..afad9dd660c
--- /dev/null
+++ b/src/include/catalog/pg_temp_statistic.h
@@ -0,0 +1,147 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_temp_statistic.h
+ *	  definition of the "temporary statistics" system catalog
+ *	  (pg_temp_statistic)
+ *
+ * This is a global temporary system catalog table storing session-specific
+ * statistics for temporary relations.  Currently, it is only used for
+ * global temporary relations.
+ *
+ * Portions Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * src/include/catalog/pg_temp_statistic.h
+ *
+ * NOTES
+ *	  The Catalog.pm module reads this file and derives schema
+ *	  information.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_TEMP_STATISTIC_H
+#define PG_TEMP_STATISTIC_H
+
+#include "catalog/genbki.h"
+#include "catalog/pg_temp_statistic_d.h"	/* IWYU pragma: export */
+
+/* ----------------
+ *		pg_temp_statistic definition.  cpp turns this into
+ *		typedef struct FormData_pg_temp_statistic
+ * ----------------
+ */
+BEGIN_CATALOG_STRUCT
+
+/*
+ * NB: The fields here must exactly match those in pg_statistic.
+ */
+CATALOG(pg_temp_statistic,8084,TempStatisticRelationId) BKI_TEMP_RELATION
+{
+	/* These fields form the unique key for the entry: */
+	Oid			starelid BKI_LOOKUP(pg_class);	/* relation containing
+												 * attribute */
+	int16		staattnum;		/* attribute (column) stats are for */
+	bool		stainherit;		/* true if inheritance children are included */
+
+	/* the fraction of the column's entries that are NULL: */
+	float4		stanullfrac;
+
+	/*
+	 * stawidth is the average width in bytes of non-null entries.  For
+	 * fixed-width datatypes this is of course the same as the typlen, but for
+	 * var-width types it is more useful.  Note that this is the average width
+	 * of the data as actually stored, post-TOASTing (eg, for a
+	 * moved-out-of-line value, only the size of the pointer object is
+	 * counted).  This is the appropriate definition for the primary use of
+	 * the statistic, which is to estimate sizes of in-memory hash tables of
+	 * tuples.
+	 */
+	int32		stawidth;
+
+	/* ----------------
+	 * stadistinct indicates the (approximate) number of distinct non-null
+	 * data values in the column.  The interpretation is:
+	 *		0		unknown or not computed
+	 *		> 0		actual number of distinct values
+	 *		< 0		negative of multiplier for number of rows
+	 * The special negative case allows us to cope with columns that are
+	 * unique (stadistinct = -1) or nearly so (for example, a column in which
+	 * non-null values appear about twice on the average could be represented
+	 * by stadistinct = -0.5 if there are no nulls, or -0.4 if 20% of the
+	 * column is nulls).  Because the number-of-rows statistic in pg_class may
+	 * be updated more frequently than pg_statistic is, it's important to be
+	 * able to describe such situations as a multiple of the number of rows,
+	 * rather than a fixed number of distinct values.  But in other cases a
+	 * fixed number is correct (eg, a boolean column).
+	 * ----------------
+	 */
+	float4		stadistinct;
+
+	/* ----------------
+	 * To allow keeping statistics on different kinds of datatypes,
+	 * we do not hard-wire any particular meaning for the remaining
+	 * statistical fields.  Instead, we provide several "slots" in which
+	 * statistical data can be placed.  Each slot includes:
+	 *		kind			integer code identifying kind of data (see below)
+	 *		op				OID of associated operator, if needed
+	 *		coll			OID of relevant collation, or 0 if none
+	 *		numbers			float4 array (for statistical values)
+	 *		values			anyarray (for representations of data values)
+	 * The ID, operator, and collation fields are never NULL; they are zeroes
+	 * in an unused slot.  The numbers and values fields are NULL in an
+	 * unused slot, and might also be NULL in a used slot if the slot kind
+	 * has no need for one or the other.
+	 * ----------------
+	 */
+
+	int16		stakind1;
+	int16		stakind2;
+	int16		stakind3;
+	int16		stakind4;
+	int16		stakind5;
+
+	Oid			staop1 BKI_LOOKUP_OPT(pg_operator);
+	Oid			staop2 BKI_LOOKUP_OPT(pg_operator);
+	Oid			staop3 BKI_LOOKUP_OPT(pg_operator);
+	Oid			staop4 BKI_LOOKUP_OPT(pg_operator);
+	Oid			staop5 BKI_LOOKUP_OPT(pg_operator);
+
+	Oid			stacoll1 BKI_LOOKUP_OPT(pg_collation);
+	Oid			stacoll2 BKI_LOOKUP_OPT(pg_collation);
+	Oid			stacoll3 BKI_LOOKUP_OPT(pg_collation);
+	Oid			stacoll4 BKI_LOOKUP_OPT(pg_collation);
+	Oid			stacoll5 BKI_LOOKUP_OPT(pg_collation);
+
+#ifdef CATALOG_VARLEN			/* variable-length fields start here */
+	float4		stanumbers1[1];
+	float4		stanumbers2[1];
+	float4		stanumbers3[1];
+	float4		stanumbers4[1];
+	float4		stanumbers5[1];
+
+	/*
+	 * Values in these arrays are values of the column's data type, or of some
+	 * related type such as an array element type.  We presently have to cheat
+	 * quite a bit to allow polymorphic arrays of this kind, but perhaps
+	 * someday it'll be a less bogus facility.
+	 */
+	anyarray	stavalues1;
+	anyarray	stavalues2;
+	anyarray	stavalues3;
+	anyarray	stavalues4;
+	anyarray	stavalues5;
+#endif
+} FormData_pg_temp_statistic;
+
+END_CATALOG_STRUCT
+
+DECLARE_TOAST(pg_temp_statistic, 8085, 8086);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_temp_statistic_relid_att_inh_index, 8087, TempStatisticRelidAttnumInhIndexId, pg_temp_statistic, btree(starelid oid_ops, staattnum int2_ops, stainherit bool_ops));
+
+MAKE_SYSCACHE(TEMPSTATRELATTINH, pg_temp_statistic_relid_att_inh_index, 128);
+
+/* Is the specified tuple from pg_temp_statistic? */
+#define IsTempStatisticTuple(tuple) \
+	((tuple)->t_tableOid == TempStatisticRelationId)
+
+#endif							/* PG_TEMP_STATISTIC_H */
diff --git a/src/test/isolation/expected/global-temp.out b/src/test/isolation/expected/global-temp.out
index a4da2d63ca3..2eac0c1d43f 100644
--- a/src/test/isolation/expected/global-temp.out
+++ b/src/test/isolation/expected/global-temp.out
@@ -569,91 +569,132 @@ key|val|seq
 
 step reset_tblspace: ALTER TABLE tmp SET TABLESPACE pg_default;
 
-starting permutation: create1 cat1 drop1 cat1
+starting permutation: create1 ins1_2 analyze1 cat1 drop1 cat1
 step create1: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text, icol int, bcol box);
-step cat1: SELECT count(*) FROM pg_temp_class WHERE oid >= 12000;
-count
------
-    3
+step ins1_2: INSERT INTO tmp2 VALUES (1, 's1');
+step analyze1: ANALYZE tmp2;
+step cat1: 
+  SELECT (SELECT count(*) FROM pg_temp_class WHERE oid >= 12000),
+         (SELECT count(*) FROM pg_temp_statistic);
+
+count|count
+-----+-----
+    3|    4
 (1 row)
 
 step drop1: DROP TABLE tmp2;
-step cat1: SELECT count(*) FROM pg_temp_class WHERE oid >= 12000;
-count
------
-    0
+step cat1: 
+  SELECT (SELECT count(*) FROM pg_temp_class WHERE oid >= 12000),
+         (SELECT count(*) FROM pg_temp_statistic);
+
+count|count
+-----+-----
+    0|    0
 (1 row)
 
 
-starting permutation: create1 cat1 drop2 cat1
+starting permutation: create1 ins1_2 analyze1 cat1 drop2 cat1
 step create1: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text, icol int, bcol box);
-step cat1: SELECT count(*) FROM pg_temp_class WHERE oid >= 12000;
-count
------
-    3
+step ins1_2: INSERT INTO tmp2 VALUES (1, 's1');
+step analyze1: ANALYZE tmp2;
+step cat1: 
+  SELECT (SELECT count(*) FROM pg_temp_class WHERE oid >= 12000),
+         (SELECT count(*) FROM pg_temp_statistic);
+
+count|count
+-----+-----
+    3|    4
 (1 row)
 
 step drop2: DROP TABLE tmp2;
-step cat1: SELECT count(*) FROM pg_temp_class WHERE oid >= 12000;
-count
------
-    0
+step cat1: 
+  SELECT (SELECT count(*) FROM pg_temp_class WHERE oid >= 12000),
+         (SELECT count(*) FROM pg_temp_statistic);
+
+count|count
+-----+-----
+    0|    0
 (1 row)
 
 
-starting permutation: create1 cat1 b1 drop2 cat1 r1 cat1
+starting permutation: create1 ins1_2 analyze1 cat1 b1 drop2 cat1 r1 cat1
 step create1: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text, icol int, bcol box);
-step cat1: SELECT count(*) FROM pg_temp_class WHERE oid >= 12000;
-count
------
-    3
+step ins1_2: INSERT INTO tmp2 VALUES (1, 's1');
+step analyze1: ANALYZE tmp2;
+step cat1: 
+  SELECT (SELECT count(*) FROM pg_temp_class WHERE oid >= 12000),
+         (SELECT count(*) FROM pg_temp_statistic);
+
+count|count
+-----+-----
+    3|    4
 (1 row)
 
 step b1: BEGIN;
 step drop2: DROP TABLE tmp2;
-step cat1: SELECT count(*) FROM pg_temp_class WHERE oid >= 12000;
-count
------
-    0
+step cat1: 
+  SELECT (SELECT count(*) FROM pg_temp_class WHERE oid >= 12000),
+         (SELECT count(*) FROM pg_temp_statistic);
+
+count|count
+-----+-----
+    0|    0
 (1 row)
 
 step r1: ROLLBACK;
-step cat1: SELECT count(*) FROM pg_temp_class WHERE oid >= 12000;
-count
------
-    0
+step cat1: 
+  SELECT (SELECT count(*) FROM pg_temp_class WHERE oid >= 12000),
+         (SELECT count(*) FROM pg_temp_statistic);
+
+count|count
+-----+-----
+    0|    0
 (1 row)
 
 
-starting permutation: create1 b1 cat1 sp1 drop2 cat1 rsp1 cat1 r1 cat1
+starting permutation: create1 ins1_2 analyze1 b1 cat1 sp1 drop2 cat1 rsp1 cat1 r1 cat1
 step create1: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text, icol int, bcol box);
+step ins1_2: INSERT INTO tmp2 VALUES (1, 's1');
+step analyze1: ANALYZE tmp2;
 step b1: BEGIN;
-step cat1: SELECT count(*) FROM pg_temp_class WHERE oid >= 12000;
-count
------
-    3
+step cat1: 
+  SELECT (SELECT count(*) FROM pg_temp_class WHERE oid >= 12000),
+         (SELECT count(*) FROM pg_temp_statistic);
+
+count|count
+-----+-----
+    3|    4
 (1 row)
 
 step sp1: SAVEPOINT sp;
 step drop2: DROP TABLE tmp2;
-step cat1: SELECT count(*) FROM pg_temp_class WHERE oid >= 12000;
-count
------
-    0
+step cat1: 
+  SELECT (SELECT count(*) FROM pg_temp_class WHERE oid >= 12000),
+         (SELECT count(*) FROM pg_temp_statistic);
+
+count|count
+-----+-----
+    0|    0
 (1 row)
 
 step rsp1: ROLLBACK TO SAVEPOINT sp;
-step cat1: SELECT count(*) FROM pg_temp_class WHERE oid >= 12000;
-count
------
-    0
+step cat1: 
+  SELECT (SELECT count(*) FROM pg_temp_class WHERE oid >= 12000),
+         (SELECT count(*) FROM pg_temp_statistic);
+
+count|count
+-----+-----
+    0|    0
 (1 row)
 
 step r1: ROLLBACK;
-step cat1: SELECT count(*) FROM pg_temp_class WHERE oid >= 12000;
-count
------
-    0
+step cat1: 
+  SELECT (SELECT count(*) FROM pg_temp_class WHERE oid >= 12000),
+         (SELECT count(*) FROM pg_temp_statistic);
+
+count|count
+-----+-----
+    0|    0
 (1 row)
 
 
diff --git a/src/test/isolation/specs/global-temp.spec b/src/test/isolation/specs/global-temp.spec
index 2df803c00fb..9eccb7e3d41 100644
--- a/src/test/isolation/specs/global-temp.spec
+++ b/src/test/isolation/specs/global-temp.spec
@@ -37,7 +37,11 @@ step alter1g { ALTER TABLE tmp2 ADD CONSTRAINT tmp2_fk FOREIGN KEY (icol) REFERE
 step alter1h { ALTER TABLE tmp2 ADD CONSTRAINT tmp2_ex EXCLUDE USING gist (bcol WITH &&); }
 step uniq_idx1 { CREATE UNIQUE INDEX tmp2_un ON tmp2(val); }
 step seltype1 { SELECT key, pg_typeof(key), val FROM tmp2; }
-step cat1 { SELECT count(*) FROM pg_temp_class WHERE oid >= 12000; }
+step analyze1 { ANALYZE tmp2; }
+step cat1 {
+  SELECT (SELECT count(*) FROM pg_temp_class WHERE oid >= 12000),
+         (SELECT count(*) FROM pg_temp_statistic);
+}
 step r1 { ROLLBACK; }
 step sp1 { SAVEPOINT sp; }
 step rsp1 { ROLLBACK TO SAVEPOINT sp; }
@@ -145,10 +149,10 @@ permutation ins1 ins2 t2 sel1 sel2 ins2 t1 sel1 sel2 ins1 t2 sel1 sel2
 permutation ins1 ins2 alt_tblspace get_tblspace1 get_tblspace2 sel1 sel2 reset_tblspace
 
 # Test global temp catalog tidy-up after DROP
-permutation create1 cat1 drop1 cat1
-permutation create1 cat1 drop2 cat1
-permutation create1 cat1 b1 drop2 cat1 r1 cat1
-permutation create1 b1 cat1 sp1 drop2 cat1 rsp1 cat1 r1 cat1
+permutation create1 ins1_2 analyze1 cat1 drop1 cat1
+permutation create1 ins1_2 analyze1 cat1 drop2 cat1
+permutation create1 ins1_2 analyze1 cat1 b1 drop2 cat1 r1 cat1
+permutation create1 ins1_2 analyze1 b1 cat1 sp1 drop2 cat1 rsp1 cat1 r1 cat1
 
 # Tidy up
 permutation drop_tblspace list_tblspaces
diff --git a/src/test/regress/expected/global_temp.out b/src/test/regress/expected/global_temp.out
index 3cfc38443b3..f0dbffb6801 100644
--- a/src/test/regress/expected/global_temp.out
+++ b/src/test/regress/expected/global_temp.out
@@ -88,11 +88,13 @@ SELECT * FROM tmp1;
 \c
 SET search_path = global_temp_tests;
 SELECT oid::regclass FROM pg_temp_class ORDER BY 1;
-           oid           
--------------------------
+                  oid                  
+---------------------------------------
  pg_temp_class
  pg_temp_class_oid_index
-(2 rows)
+ pg_temp_statistic
+ pg_temp_statistic_relid_att_inh_index
+(4 rows)
 
 SELECT * FROM tmp1;
  a | b | c 
@@ -100,13 +102,15 @@ SELECT * FROM tmp1;
 (0 rows)
 
 SELECT oid::regclass FROM pg_temp_class ORDER BY 1;
-           oid           
--------------------------
+                  oid                  
+---------------------------------------
  pg_temp_class
  pg_temp_class_oid_index
+ pg_temp_statistic
+ pg_temp_statistic_relid_att_inh_index
  tmp1
  tmp1_pkey
-(4 rows)
+(6 rows)
 
 -- Test pg_relation_filenode() matches global relfilenode
 SELECT relfilenode = pg_relation_filenode('tmp1'::regclass) AS ok
@@ -527,14 +531,16 @@ ROLLBACK TO sp;
 INSERT INTO tmp1 VALUES (1, 'xxx');
 COMMIT;
 SELECT oid::regclass FROM pg_temp_class ORDER BY 1;
-           oid           
--------------------------
+                  oid                  
+---------------------------------------
  pg_temp_class
  pg_temp_class_oid_index
+ pg_temp_statistic
+ pg_temp_statistic_relid_att_inh_index
  tmp1_c_seq
  tmp1
  tmp1_pkey
-(5 rows)
+(7 rows)
 
 SELECT * FROM tmp1;
  a |  b  | c 
@@ -749,6 +755,43 @@ SELECT oid::regclass, relpages, reltuples, relallvisible, relallfrozen
  tmp2 |        5 |       150 |            10 |           20
 (1 row)
 
+DROP TABLE tmp2;
+-- Test column stats
+CREATE GLOBAL TEMP TABLE tmp2 (a int, b int);
+INSERT INTO tmp2 SELECT x, floor(sqrt(x)) FROM generate_series(36, 99) x;
+ANALYZE tmp2;
+SELECT stavalues1 FROM pg_statistic
+ WHERE starelid = 'tmp2'::regclass AND staattnum = 2;
+ stavalues1 
+------------
+(0 rows)
+
+SELECT stavalues1 FROM pg_temp_statistic
+ WHERE starelid = 'tmp2'::regclass AND staattnum = 2;
+ stavalues1 
+------------
+ {9,8,7,6}
+(1 row)
+
+SELECT most_common_vals FROM pg_stats
+ WHERE tablename = 'tmp2' AND attname = 'b';
+ most_common_vals 
+------------------
+ {9,8,7,6}
+(1 row)
+
+SELECT COUNT(*) FROM tmp2 WHERE b = 8;
+ count 
+-------
+    17
+(1 row)
+
+SELECT row_estimate('SELECT * FROM tmp2 WHERE b = 8');
+ row_estimate 
+--------------
+           17
+(1 row)
+
 DROP TABLE tmp2;
 -- Test view creation
 CREATE VIEW v AS SELECT * FROM tmp1;
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 3c3404e51b8..be2efa78257 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -287,3 +287,14 @@ NOTICE:  checking pg_propgraph_property {pgptypid} => pg_type {oid}
 NOTICE:  checking pg_propgraph_property {pgpcollation} => pg_collation {oid}
 NOTICE:  checking pg_temp_class {oid} => pg_class {oid}
 NOTICE:  checking pg_temp_class {reltablespace} => pg_tablespace {oid}
+NOTICE:  checking pg_temp_statistic {starelid} => pg_class {oid}
+NOTICE:  checking pg_temp_statistic {staop1} => pg_operator {oid}
+NOTICE:  checking pg_temp_statistic {staop2} => pg_operator {oid}
+NOTICE:  checking pg_temp_statistic {staop3} => pg_operator {oid}
+NOTICE:  checking pg_temp_statistic {staop4} => pg_operator {oid}
+NOTICE:  checking pg_temp_statistic {staop5} => pg_operator {oid}
+NOTICE:  checking pg_temp_statistic {stacoll1} => pg_collation {oid}
+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}
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index e3469b34d4d..b5ecc98cc2c 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1302,6 +1302,71 @@ pg_aios| SELECT pid,
     f_localmem,
     f_buffered
    FROM pg_get_aios() pg_get_aios(pid, io_id, io_generation, state, operation, off, length, target, handle_data_len, raw_result, result, target_desc, f_sync, f_localmem, f_buffered);
+pg_all_statistic| SELECT pg_statistic.starelid,
+    pg_statistic.staattnum,
+    pg_statistic.stainherit,
+    pg_statistic.stanullfrac,
+    pg_statistic.stawidth,
+    pg_statistic.stadistinct,
+    pg_statistic.stakind1,
+    pg_statistic.stakind2,
+    pg_statistic.stakind3,
+    pg_statistic.stakind4,
+    pg_statistic.stakind5,
+    pg_statistic.staop1,
+    pg_statistic.staop2,
+    pg_statistic.staop3,
+    pg_statistic.staop4,
+    pg_statistic.staop5,
+    pg_statistic.stacoll1,
+    pg_statistic.stacoll2,
+    pg_statistic.stacoll3,
+    pg_statistic.stacoll4,
+    pg_statistic.stacoll5,
+    pg_statistic.stanumbers1,
+    pg_statistic.stanumbers2,
+    pg_statistic.stanumbers3,
+    pg_statistic.stanumbers4,
+    pg_statistic.stanumbers5,
+    pg_statistic.stavalues1,
+    pg_statistic.stavalues2,
+    pg_statistic.stavalues3,
+    pg_statistic.stavalues4,
+    pg_statistic.stavalues5
+   FROM pg_statistic
+UNION ALL
+ SELECT pg_temp_statistic.starelid,
+    pg_temp_statistic.staattnum,
+    pg_temp_statistic.stainherit,
+    pg_temp_statistic.stanullfrac,
+    pg_temp_statistic.stawidth,
+    pg_temp_statistic.stadistinct,
+    pg_temp_statistic.stakind1,
+    pg_temp_statistic.stakind2,
+    pg_temp_statistic.stakind3,
+    pg_temp_statistic.stakind4,
+    pg_temp_statistic.stakind5,
+    pg_temp_statistic.staop1,
+    pg_temp_statistic.staop2,
+    pg_temp_statistic.staop3,
+    pg_temp_statistic.staop4,
+    pg_temp_statistic.staop5,
+    pg_temp_statistic.stacoll1,
+    pg_temp_statistic.stacoll2,
+    pg_temp_statistic.stacoll3,
+    pg_temp_statistic.stacoll4,
+    pg_temp_statistic.stacoll5,
+    pg_temp_statistic.stanumbers1,
+    pg_temp_statistic.stanumbers2,
+    pg_temp_statistic.stanumbers3,
+    pg_temp_statistic.stanumbers4,
+    pg_temp_statistic.stanumbers5,
+    pg_temp_statistic.stavalues1,
+    pg_temp_statistic.stavalues2,
+    pg_temp_statistic.stavalues3,
+    pg_temp_statistic.stavalues4,
+    pg_temp_statistic.stavalues5
+   FROM pg_temp_statistic;
 pg_available_extension_versions| SELECT e.name,
     e.version,
     (x.extname IS NOT NULL) AS installed,
@@ -2705,7 +2770,7 @@ pg_stats| SELECT n.nspname AS schemaname,
             WHEN (s.stakind5 = 7) THEN s.stavalues5
             ELSE NULL::anyarray
         END AS range_bounds_histogram
-   FROM (((pg_statistic s
+   FROM (((pg_all_statistic s
      JOIN pg_class c ON ((c.oid = s.starelid)))
      JOIN pg_attribute a ON (((c.oid = a.attrelid) AND (a.attnum = s.staattnum))))
      LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace)))
diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out
index 8370c1561cc..5f5756d5596 100644
--- a/src/test/regress/expected/sanity_check.out
+++ b/src/test/regress/expected/sanity_check.out
@@ -25,3 +25,26 @@ SELECT relname, relkind
 ---------+---------
 (0 rows)
 
+-- check that pg_statistic and pg_temp_statistic 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'::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'::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 eae3f3d4698..9d414045629 100644
--- a/src/test/regress/sql/global_temp.sql
+++ b/src/test/regress/sql/global_temp.sql
@@ -422,6 +422,25 @@ SELECT oid::regclass, relpages, reltuples, relallvisible, relallfrozen
 
 DROP TABLE tmp2;
 
+-- Test column stats
+CREATE GLOBAL TEMP TABLE tmp2 (a int, b int);
+INSERT INTO tmp2 SELECT x, floor(sqrt(x)) FROM generate_series(36, 99) x;
+ANALYZE tmp2;
+
+SELECT stavalues1 FROM pg_statistic
+ WHERE starelid = 'tmp2'::regclass AND staattnum = 2;
+
+SELECT stavalues1 FROM pg_temp_statistic
+ WHERE starelid = 'tmp2'::regclass AND staattnum = 2;
+
+SELECT most_common_vals FROM pg_stats
+ WHERE tablename = 'tmp2' AND attname = 'b';
+
+SELECT COUNT(*) FROM tmp2 WHERE b = 8;
+SELECT row_estimate('SELECT * FROM tmp2 WHERE b = 8');
+
+DROP TABLE tmp2;
+
 -- Test view creation
 CREATE VIEW v AS SELECT * FROM tmp1;
 SELECT * FROM v;
diff --git a/src/test/regress/sql/sanity_check.sql b/src/test/regress/sql/sanity_check.sql
index 162e5324b5d..ed0096d7823 100644
--- a/src/test/regress/sql/sanity_check.sql
+++ b/src/test/regress/sql/sanity_check.sql
@@ -19,3 +19,23 @@ SELECT relname, relkind
   FROM pg_class
  WHERE relkind IN ('v', 'c', 'f', 'p', 'I')
        AND relfilenode <> 0;
+
+-- check that pg_statistic and pg_temp_statistic 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'::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'::regclass
+)
+(SELECT * FROM t1 EXCEPT SELECT * FROM t2)
+UNION ALL
+(SELECT * FROM t2 EXCEPT SELECT * FROM t1);
-- 
2.43.0

