From 5b8eb793569590f989ff70e1ddb8521a876bdaa6 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 v11 7/9] Allow catalog tables to be global temporary and add
 pg_temp_statistic.

This commit allows system catalog tables to be global temporary
tables, and adds the first such example: pg_temp_statistic. 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/bootstrap/bootparse.y           |  23 ++-
 src/backend/bootstrap/bootscanner.l         |   1 +
 src/backend/catalog/Catalog.pm              |   2 +
 src/backend/catalog/genbki.pl               |  44 +++++
 src/backend/catalog/global_temp.c           |  51 +++++-
 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/activity/pgstat_io.c      |  11 +-
 src/backend/utils/adt/selfuncs.c            |  18 +-
 src/backend/utils/cache/lsyscache.c         |  12 +-
 src/backend/utils/cache/relcache.c          |  11 ++
 src/backend/utils/cache/syscache.c          |   7 +-
 src/include/catalog/Makefile                |   3 +-
 src/include/catalog/genbki.h                |   1 +
 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 | 190 +++++++++++++++-----
 src/test/isolation/specs/global-temp.spec   |  10 +-
 src/test/recovery/t/018_wal_optimize.pl     |   1 +
 src/test/regress/expected/global_temp.out   |  63 +++++--
 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/expected/stats.out         |   3 +-
 src/test/regress/sql/global_temp.sql        |  19 ++
 src/test/regress/sql/sanity_check.sql       |  20 +++
 37 files changed, 836 insertions(+), 134 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 b3df993317b..23ac8b82cdb 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -325,6 +325,11 @@
       <entry>tablespaces within this database cluster</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>
@@ -7663,6 +7668,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">
@@ -8554,6 +8569,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 81315b3fa46..aa781041418 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 e21635b385c..77438f386f7 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -1326,6 +1326,7 @@ WHERE relname LIKE 'tenk1%';
 
   <indexterm>
    <primary>pg_statistic</primary>
+   <secondary>pg_temp_statistic</secondary>
   </indexterm>
 
   <para>
@@ -1337,7 +1338,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.
@@ -1348,13 +1352,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
@@ -1407,6 +1413,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
@@ -1416,7 +1423,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 ca61db63361..2694ce9f37f 100644
--- a/doc/src/sgml/planstats.sgml
+++ b/doc/src/sgml/planstats.sgml
@@ -108,7 +108,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:
 
@@ -719,7 +721,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
@@ -727,12 +730,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>
 
@@ -766,7 +771,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/bootstrap/bootparse.y b/src/backend/bootstrap/bootparse.y
index 305a5654ff3..7026a138375 100644
--- a/src/backend/bootstrap/bootparse.y
+++ b/src/backend/bootstrap/bootparse.y
@@ -96,7 +96,7 @@ static int num_columns_read = 0;
 %type <list>  boot_index_params
 %type <ielem> boot_index_param
 %type <str>   boot_ident
-%type <ival>  optbootstrap optsharedrelation boot_column_nullness
+%type <ival>  optbootstrap optsharedrelation opttemprelation boot_column_nullness
 %type <oidval> oidspec optrowtypeoid
 
 %token <str> ID
@@ -106,7 +106,7 @@ static int num_columns_read = 0;
 /* All the rest are unreserved, and should be handled in boot_ident! */
 %token <kw> OPEN XCLOSE XCREATE INSERT_TUPLE
 %token <kw> XDECLARE INDEX ON USING XBUILD INDICES UNIQUE XTOAST
-%token <kw> OBJ_ID XBOOTSTRAP XSHARED_RELATION XROWTYPE_OID
+%token <kw> OBJ_ID XBOOTSTRAP XSHARED_RELATION XTEMP_RELATION XROWTYPE_OID
 %token <kw> XFORCE XNOT XNULL
 
 %start TopLevel
@@ -155,13 +155,14 @@ Boot_CloseStmt:
 		;
 
 Boot_CreateStmt:
-		  XCREATE boot_ident oidspec optbootstrap optsharedrelation optrowtypeoid LPAREN
+		  XCREATE boot_ident oidspec optbootstrap optsharedrelation opttemprelation optrowtypeoid LPAREN
 				{
 					do_start();
 					numattr = 0;
-					elog(DEBUG4, "creating%s%s relation %s %u",
+					elog(DEBUG4, "creating%s%s%s relation %s %u",
 						 $4 ? " bootstrap" : "",
 						 $5 ? " shared" : "",
+						 $6 ? " global temp" : "",
 						 $2,
 						 $3);
 				}
@@ -173,6 +174,7 @@ Boot_CreateStmt:
 				{
 					TupleDesc	tupdesc;
 					bool		shared_relation;
+					bool		temp_relation;
 					bool		mapped_relation;
 
 					do_start();
@@ -180,6 +182,7 @@ Boot_CreateStmt:
 					tupdesc = CreateTupleDesc(numattr, attrtypes);
 
 					shared_relation = $5;
+					temp_relation = $6;
 
 					/*
 					 * The catalogs that use the relation mapper are the
@@ -211,6 +214,8 @@ Boot_CreateStmt:
 												   HEAP_TABLE_AM_OID,
 												   tupdesc,
 												   RELKIND_RELATION,
+												   temp_relation ?
+												   RELPERSISTENCE_GLOBAL_TEMP :
 												   RELPERSISTENCE_PERMANENT,
 												   shared_relation,
 												   mapped_relation,
@@ -229,13 +234,15 @@ Boot_CreateStmt:
 													  PG_CATALOG_NAMESPACE,
 													  shared_relation ? GLOBALTABLESPACE_OID : 0,
 													  $3,
-													  $6,
+													  $7,
 													  InvalidOid,
 													  BOOTSTRAP_SUPERUSERID,
 													  HEAP_TABLE_AM_OID,
 													  tupdesc,
 													  NIL,
 													  RELKIND_RELATION,
+													  temp_relation ?
+													  RELPERSISTENCE_GLOBAL_TEMP :
 													  RELPERSISTENCE_PERMANENT,
 													  shared_relation,
 													  mapped_relation,
@@ -433,6 +440,11 @@ optsharedrelation:
 		|						{ $$ = 0; }
 		;
 
+opttemprelation:
+			XTEMP_RELATION	{ $$ = 1; }
+		|					{ $$ = 0; }
+		;
+
 optrowtypeoid:
 			XROWTYPE_OID oidspec	{ $$ = $2; }
 		|							{ $$ = InvalidOid; }
@@ -492,6 +504,7 @@ boot_ident:
 		| OBJ_ID		{ $$ = pstrdup($1); }
 		| XBOOTSTRAP	{ $$ = pstrdup($1); }
 		| XSHARED_RELATION	{ $$ = pstrdup($1); }
+		| XTEMP_RELATION	{ $$ = pstrdup($1); }
 		| XROWTYPE_OID	{ $$ = pstrdup($1); }
 		| XFORCE		{ $$ = pstrdup($1); }
 		| XNOT			{ $$ = pstrdup($1); }
diff --git a/src/backend/bootstrap/bootscanner.l b/src/backend/bootstrap/bootscanner.l
index 9674f2795d1..f8c1a671712 100644
--- a/src/backend/bootstrap/bootscanner.l
+++ b/src/backend/bootstrap/bootscanner.l
@@ -82,6 +82,7 @@ create			{ yylval->kw = "create"; return XCREATE; }
 OID				{ yylval->kw = "OID"; return OBJ_ID; }
 bootstrap		{ yylval->kw = "bootstrap"; return XBOOTSTRAP; }
 shared_relation	{ yylval->kw = "shared_relation"; return XSHARED_RELATION; }
+temp_relation	{ yylval->kw = "temp_relation"; return XTEMP_RELATION; }
 rowtype_oid		{ yylval->kw = "rowtype_oid"; return XROWTYPE_OID; }
 
 insert			{ yylval->kw = "insert"; return INSERT_TUPLE; }
diff --git a/src/backend/catalog/Catalog.pm b/src/backend/catalog/Catalog.pm
index 219af5884d9..78e69b3f0d3 100644
--- a/src/backend/catalog/Catalog.pm
+++ b/src/backend/catalog/Catalog.pm
@@ -176,6 +176,8 @@ sub ParseHeader
 			$catalog{bootstrap} = /BKI_BOOTSTRAP/ ? ' bootstrap' : '';
 			$catalog{shared_relation} =
 			  /BKI_SHARED_RELATION/ ? ' shared_relation' : '';
+			$catalog{temp_relation} =
+			  /BKI_TEMP_RELATION/ ? ' temp_relation' : '';
 			if (/BKI_ROWTYPE_OID\(\s*
 				 (?<rowtype_oid>\d+),\s*
 				 (?<rowtype_oid_macro>\w+)\s*
diff --git a/src/backend/catalog/genbki.pl b/src/backend/catalog/genbki.pl
index 86f3135f9c7..20d4f142a75 100644
--- a/src/backend/catalog/genbki.pl
+++ b/src/backend/catalog/genbki.pl
@@ -174,6 +174,7 @@ foreach my $header (@ARGV)
 			index_oid_macro => $index->{index_oid_macro},
 			key => $key,
 			nbuckets => $syscache->{syscache_nbuckets},
+			table_is_temp => $catalogs{$tblname}->{temp_relation} eq "" ? 0 : 1,
 		};
 
 		$syscache_catalogs{$catname} = 1;
@@ -518,6 +519,7 @@ EOM
 	# .bki CREATE command for this catalog
 	print $bki "create $catname $catalog->{relation_oid}"
 	  . $catalog->{shared_relation}
+	  . $catalog->{temp_relation}
 	  . $catalog->{bootstrap}
 	  . $catalog->{rowtype_oid_clause};
 
@@ -798,6 +800,8 @@ print_boilerplate($syscache_ids_fh, "syscache_ids.h", "SysCache identifiers");
 print $syscache_ids_fh "#ifndef SYSCACHE_IDS_H
 #define SYSCACHE_IDS_H
 
+#include \"catalog/pg_temp_statistic_d.h\"
+
 typedef enum SysCacheIdentifier
 {
 \tSYSCACHEID_INVALID = -1,\n";
@@ -838,6 +842,46 @@ foreach my $syscache (sort keys %syscaches)
 print $syscache_ids_fh "} SysCacheIdentifier;\n";
 print $syscache_ids_fh "#define SysCacheSize ($last_syscache + 1)\n\n";
 
+# Macro to test if a catalog relation is a global temporary table or index
+print $syscache_ids_fh "/* Is the specified catalog relation a global temporary table or index? */\n";
+print $syscache_ids_fh "#define IsGlobalTempCatalogRelation(relid) \\\n";
+
+my $num_clauses = 0;
+foreach my $catname (sort keys %catalogs)
+{
+	my $catalog = $catalogs{$catname};
+
+	if ($catalog->{temp_relation})
+	{
+		print $syscache_ids_fh $num_clauses == 0 ? "\t(" : " || \\\n\t ";
+		print $syscache_ids_fh "(relid) == $catalog->{relation_oid_macro}";
+		$num_clauses++;
+
+		foreach my $index (@{ $catalog->{indexing} })
+		{
+			print $syscache_ids_fh " || \\\n\t (relid) == $index->{index_oid_macro}";
+			$num_clauses++;
+		}
+	}
+}
+print $syscache_ids_fh $num_clauses == 0 ? "false\n\n" : ")\n\n";
+
+# Macro to test if a syscache's catalog table is global temporary
+print $syscache_ids_fh "/* Does the specified SysCache use a global temporary table? */\n";
+print $syscache_ids_fh "#define SysCacheTableIsGlobalTemp(cacheId) \\\n";
+
+$num_clauses = 0;
+foreach my $syscache (sort keys %syscaches)
+{
+	if ($syscaches{$syscache}{table_is_temp})
+	{
+		print $syscache_ids_fh $num_clauses == 0 ? "\t(" : " || \\\n\t ";
+		print $syscache_ids_fh "(cacheId) == $syscache";
+		$num_clauses++;
+	}
+}
+print $syscache_ids_fh $num_clauses == 0 ? "false\n\n" : ")\n\n";
+
 # Closing boilerplate for syscache_ids.h
 print $syscache_ids_fh "#endif\t\t\t\t\t\t\t/* SYSCACHE_IDS_H */\n";
 
diff --git a/src/backend/catalog/global_temp.c b/src/backend/catalog/global_temp.c
index 5c253bb5be6..318721a84ca 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/storage.h"
 #include "commands/sequence.h"
 #include "commands/tablecmds.h"
@@ -71,6 +72,7 @@
 #include "storage/proc.h"
 #include "storage/shmem.h"
 #include "storage/subsystems.h"
+#include "utils/fmgroids.h"
 #include "utils/fmgrprotos.h"
 #include "utils/memutils.h"
 #include "utils/syscache.h"
@@ -1240,6 +1242,10 @@ ProcessInvalidatedGlobalTempRelations(void)
 	 */
 	if (gtrs_dropped && processed_dropped_subid == InvalidSubTransactionId)
 	{
+		int			orig_xact_flags = MyXactFlags;
+		bool		tuples_deleted = false;
+		Relation	statrel;
+
 		/*
 		 * Delete and forget locally-created storage for dropped relations.
 		 * This is done non-transactionally, since gtrs_dropped contains only
@@ -1271,16 +1277,55 @@ ProcessInvalidatedGlobalTempRelations(void)
 		}
 
 		/*
-		 * Remove all usage records and forget any ON COMMIT actions for the
-		 * dropped relations.  The former is non-transactional, but the latter
-		 * may be undone by a (sub)rollback.
+		 * Remove all usage records, forget any ON COMMIT actions, and delete
+		 * any temporary catalog entries for the dropped relations.  The usage
+		 * 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);
+
+			/* 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();
+
+		/*
+		 * Reset XACT_FLAGS_ACCESSEDTEMPNAMESPACE, if it wasn't set on entry,
+		 * otherwise PREPARE TRANSACTION would fail for this transaction, even
+		 * if the user hadn't explicitly accessed any temporary relations.
+		 */
+		if ((orig_xact_flags & XACT_FLAGS_ACCESSEDTEMPNAMESPACE) == 0)
+			MyXactFlags &= ~XACT_FLAGS_ACCESSEDTEMPNAMESPACE;
+
 		/* Update this backend's tempfrozenxid and tempminmxid */
 		UpdateTempFrozenXids();
 
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 55dd497a116..93cc874257f 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 7b37a3a0b79..4bd89f2feba 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..1c26ce6807e 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 row or the
+ *		session-local GtrInfo struct for the relation have 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/GtrInfo 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 c35892ce6d0..e93a4d3c61a 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"
@@ -217,6 +218,7 @@ attribute_statistics_update_internal(Oid reloid,
 									 bool inherited, FunctionCallInfo fcinfo)
 {
 	Relation	starel;
+	SysCacheIdentifier cacheId;
 	HeapTuple	statup;
 
 	Oid			atttypid = InvalidOid;
@@ -349,9 +351,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))
@@ -593,12 +604,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/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 8ec1aad5078..c9fb53c10de 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -413,18 +413,17 @@ pgstat_tracks_io_object(BackendType bktype, IOObject io_object,
 		return false;
 
 	/*
-	 * In core Postgres, only regular backends and WAL Sender processes
-	 * executing queries will use local buffers and operate on temporary
-	 * relations. Parallel workers will not use local buffers (see
+	 * In core Postgres, only initdb, regular backends, and WAL Sender
+	 * processes executing queries will use local buffers and operate on
+	 * temporary relations. Parallel workers will not use local buffers (see
 	 * InitLocalBuffers()); however, extensions leveraging background workers
 	 * have no such limitation, so track IO on IOOBJECT_TEMP_RELATION for
 	 * BackendType B_BG_WORKER.
 	 */
 	no_temp_rel = bktype == B_AUTOVAC_LAUNCHER || bktype == B_BG_WRITER ||
 		bktype == B_CHECKPOINTER || bktype == B_AUTOVAC_WORKER ||
-		bktype == B_STANDALONE_BACKEND || bktype == B_STARTUP ||
-		bktype == B_WAL_SUMMARIZER || bktype == B_WAL_WRITER ||
-		bktype == B_WAL_RECEIVER;
+		bktype == B_STARTUP || bktype == B_WAL_SUMMARIZER ||
+		bktype == B_WAL_WRITER || bktype == B_WAL_RECEIVER;
 
 	if (no_temp_rel && io_context == IOCONTEXT_NORMAL &&
 		io_object == IOOBJECT_TEMP_RELATION)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index 5ee377595bd..47ee254f6ac 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -5837,7 +5837,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));
@@ -6067,7 +6068,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));
@@ -6636,7 +6638,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));
@@ -6662,7 +6665,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));
@@ -9166,7 +9170,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));
@@ -9196,7 +9201,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 2d84875ed23..92998d5037f 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -39,6 +39,7 @@
 #include "catalog/pg_range.h"
 #include "catalog/pg_statistic.h"
 #include "catalog/pg_subscription.h"
+#include "catalog/pg_temp_statistic.h"
 #include "catalog/pg_transform.h"
 #include "catalog/pg_type.h"
 #include "miscadmin.h"
@@ -3501,7 +3502,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));
@@ -3595,7 +3597,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);
 
 		/*
@@ -3640,7 +3644,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/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 56636001b37..cdeb7927388 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -7024,6 +7024,15 @@ write_item(const void *data, Size len, FILE *fp)
  * of the latter. The special cases are relations where
  * RelationCacheInitializePhase2/3 chooses to nail for efficiency reasons, but
  * which do not support any syscache.
+ *
+ * Global temporary relations are never nailed (because that would required
+ * them to be mapped, and the relmapper does not support temporary relations),
+ * but they do all support syscaches.  Despite this, we intentionally do not
+ * cache global temporary relations, since we don't want to load them on
+ * startup, because doing so would result in temporary relation storage being
+ * created when it might not be needed.  Instead, all global temporary
+ * relations are lazily initialized, if and when they are needed.  See also
+ * InitCatalogCachePhase2().
  */
 bool
 RelationIdIsInInitFile(Oid relationId)
@@ -7040,6 +7049,8 @@ RelationIdIsInInitFile(Oid relationId)
 		Assert(!RelationSupportsSysCache(relationId));
 		return true;
 	}
+	if (IsGlobalTempCatalogRelation(relationId))
+		return false;
 	return RelationSupportsSysCache(relationId);
 }
 
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index f4233f9e31a..d08c9088b71 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -176,6 +176,10 @@ InitCatalogCache(void)
  * relcache with entries for the most-commonly-used system catalogs.
  * Therefore, we invoke this routine when we need to write a new relcache
  * init file.
+ *
+ * We skip caches based on global temporary relations because we don't want
+ * temporary relation storage to be needlessly created on startup.  Instead,
+ * always initialize these caches on first use.
  */
 void
 InitCatalogCachePhase2(void)
@@ -185,7 +189,8 @@ InitCatalogCachePhase2(void)
 	Assert(CacheInitialized);
 
 	for (cacheId = 0; cacheId < SysCacheSize; cacheId++)
-		InitCatCachePhase2(SysCache[cacheId], true);
+		if (!SysCacheTableIsGlobalTemp(cacheId))
+			InitCatCachePhase2(SysCache[cacheId], true);
 }
 
 
diff --git a/src/include/catalog/Makefile b/src/include/catalog/Makefile
index 444fc76eed6..6bfb60af0c9 100644
--- a/src/include/catalog/Makefile
+++ b/src/include/catalog/Makefile
@@ -81,7 +81,8 @@ CATALOG_HEADERS := \
 	pg_publication_namespace.h \
 	pg_publication_rel.h \
 	pg_subscription.h \
-	pg_subscription_rel.h
+	pg_subscription_rel.h \
+	pg_temp_statistic.h
 
 GENERATED_HEADERS := $(CATALOG_HEADERS:%.h=%_d.h)
 
diff --git a/src/include/catalog/genbki.h b/src/include/catalog/genbki.h
index 12d2a3e295b..2f1253281c5 100644
--- a/src/include/catalog/genbki.h
+++ b/src/include/catalog/genbki.h
@@ -44,6 +44,7 @@
 /* Options that may appear after CATALOG (on the same line) */
 #define BKI_BOOTSTRAP
 #define BKI_SHARED_RELATION
+#define BKI_TEMP_RELATION
 #define BKI_ROWTYPE_OID(oid,oidmacro)
 #define BKI_SCHEMA_MACRO
 
diff --git a/src/include/catalog/meson.build b/src/include/catalog/meson.build
index bcc01c87c2e..be51883747a 100644
--- a/src/include/catalog/meson.build
+++ b/src/include/catalog/meson.build
@@ -69,6 +69,7 @@ catalog_headers = [
   'pg_publication_rel.h',
   'pg_subscription.h',
   'pg_subscription_rel.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..1b90608f276
--- /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,8085,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, 8086, 8087);
+
+DECLARE_UNIQUE_INDEX_PKEY(pg_temp_statistic_relid_att_inh_index, 8088, 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 f79cf24e1c4..223f3519c2d 100644
--- a/src/test/isolation/expected/global-temp.out
+++ b/src/test/isolation/expected/global-temp.out
@@ -453,72 +453,115 @@ key|val|seq
 
 step reset_tblspace: ALTER TABLE tmp SET TABLESPACE pg_default;
 
-starting permutation: create1 ins1_2 used1 drop1 used1
+starting permutation: create1 ins1_2 analyze1 used1 drop1 used1
 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 used1: 
   SELECT regexp_replace(oid::regclass::text, '_(\d+)', '_NNN', 'g')
     FROM pg_gtrs_in_use()
    ORDER BY 1;
+  SELECT count(*) FROM pg_temp_statistic;
 
-regexp_replace             
----------------------------
-pg_toast.pg_toast_NNN      
-pg_toast.pg_toast_NNN_index
-tmp2                       
-(3 rows)
+regexp_replace                       
+-------------------------------------
+pg_temp_statistic                    
+pg_temp_statistic_relid_att_inh_index
+pg_toast.pg_toast_NNN                
+pg_toast.pg_toast_NNN_index          
+tmp2                                 
+(5 rows)
+
+count
+-----
+    4
+(1 row)
 
 step drop1: DROP TABLE tmp2;
 step used1: 
   SELECT regexp_replace(oid::regclass::text, '_(\d+)', '_NNN', 'g')
     FROM pg_gtrs_in_use()
    ORDER BY 1;
+  SELECT count(*) FROM pg_temp_statistic;
 
-regexp_replace
---------------
-(0 rows)
+regexp_replace                       
+-------------------------------------
+pg_temp_statistic                    
+pg_temp_statistic_relid_att_inh_index
+(2 rows)
+
+count
+-----
+    0
+(1 row)
 
 
-starting permutation: create1 ins1_2 used1 drop2 used1
+starting permutation: create1 ins1_2 analyze1 used1 drop2 used1
 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 used1: 
   SELECT regexp_replace(oid::regclass::text, '_(\d+)', '_NNN', 'g')
     FROM pg_gtrs_in_use()
    ORDER BY 1;
+  SELECT count(*) FROM pg_temp_statistic;
 
-regexp_replace             
----------------------------
-pg_toast.pg_toast_NNN      
-pg_toast.pg_toast_NNN_index
-tmp2                       
-(3 rows)
+regexp_replace                       
+-------------------------------------
+pg_temp_statistic                    
+pg_temp_statistic_relid_att_inh_index
+pg_toast.pg_toast_NNN                
+pg_toast.pg_toast_NNN_index          
+tmp2                                 
+(5 rows)
+
+count
+-----
+    4
+(1 row)
 
 step drop2: DROP TABLE tmp2;
 step used1: 
   SELECT regexp_replace(oid::regclass::text, '_(\d+)', '_NNN', 'g')
     FROM pg_gtrs_in_use()
    ORDER BY 1;
+  SELECT count(*) FROM pg_temp_statistic;
 
-regexp_replace
---------------
-(0 rows)
+regexp_replace                       
+-------------------------------------
+pg_temp_statistic                    
+pg_temp_statistic_relid_att_inh_index
+(2 rows)
 
+count
+-----
+    0
+(1 row)
 
-starting permutation: create1 ins1_2 used1 b1 drop2 used1 r1 used1
+
+starting permutation: create1 ins1_2 analyze1 used1 b1 drop2 used1 r1 used1
 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 used1: 
   SELECT regexp_replace(oid::regclass::text, '_(\d+)', '_NNN', 'g')
     FROM pg_gtrs_in_use()
    ORDER BY 1;
+  SELECT count(*) FROM pg_temp_statistic;
 
-regexp_replace             
----------------------------
-pg_toast.pg_toast_NNN      
-pg_toast.pg_toast_NNN_index
-tmp2                       
-(3 rows)
+regexp_replace                       
+-------------------------------------
+pg_temp_statistic                    
+pg_temp_statistic_relid_att_inh_index
+pg_toast.pg_toast_NNN                
+pg_toast.pg_toast_NNN_index          
+tmp2                                 
+(5 rows)
+
+count
+-----
+    4
+(1 row)
 
 step b1: BEGIN;
 step drop2: DROP TABLE tmp2;
@@ -526,37 +569,62 @@ step used1:
   SELECT regexp_replace(oid::regclass::text, '_(\d+)', '_NNN', 'g')
     FROM pg_gtrs_in_use()
    ORDER BY 1;
+  SELECT count(*) FROM pg_temp_statistic;
 
-regexp_replace
---------------
-(0 rows)
+regexp_replace                       
+-------------------------------------
+pg_temp_statistic                    
+pg_temp_statistic_relid_att_inh_index
+(2 rows)
+
+count
+-----
+    0
+(1 row)
 
 step r1: ROLLBACK;
 step used1: 
   SELECT regexp_replace(oid::regclass::text, '_(\d+)', '_NNN', 'g')
     FROM pg_gtrs_in_use()
    ORDER BY 1;
+  SELECT count(*) FROM pg_temp_statistic;
 
-regexp_replace
---------------
-(0 rows)
+regexp_replace                       
+-------------------------------------
+pg_temp_statistic                    
+pg_temp_statistic_relid_att_inh_index
+(2 rows)
+
+count
+-----
+    0
+(1 row)
 
 
-starting permutation: create1 ins1_2 b1 used1 sp1 drop2 used1 rsp1 used1 r1 used1
+starting permutation: create1 ins1_2 analyze1 b1 used1 sp1 drop2 used1 rsp1 used1 r1 used1
 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 used1: 
   SELECT regexp_replace(oid::regclass::text, '_(\d+)', '_NNN', 'g')
     FROM pg_gtrs_in_use()
    ORDER BY 1;
+  SELECT count(*) FROM pg_temp_statistic;
 
-regexp_replace             
----------------------------
-pg_toast.pg_toast_NNN      
-pg_toast.pg_toast_NNN_index
-tmp2                       
-(3 rows)
+regexp_replace                       
+-------------------------------------
+pg_temp_statistic                    
+pg_temp_statistic_relid_att_inh_index
+pg_toast.pg_toast_NNN                
+pg_toast.pg_toast_NNN_index          
+tmp2                                 
+(5 rows)
+
+count
+-----
+    4
+(1 row)
 
 step sp1: SAVEPOINT sp;
 step drop2: DROP TABLE tmp2;
@@ -564,30 +632,54 @@ step used1:
   SELECT regexp_replace(oid::regclass::text, '_(\d+)', '_NNN', 'g')
     FROM pg_gtrs_in_use()
    ORDER BY 1;
+  SELECT count(*) FROM pg_temp_statistic;
 
-regexp_replace
---------------
-(0 rows)
+regexp_replace                       
+-------------------------------------
+pg_temp_statistic                    
+pg_temp_statistic_relid_att_inh_index
+(2 rows)
+
+count
+-----
+    0
+(1 row)
 
 step rsp1: ROLLBACK TO SAVEPOINT sp;
 step used1: 
   SELECT regexp_replace(oid::regclass::text, '_(\d+)', '_NNN', 'g')
     FROM pg_gtrs_in_use()
    ORDER BY 1;
+  SELECT count(*) FROM pg_temp_statistic;
 
-regexp_replace
---------------
-(0 rows)
+regexp_replace                       
+-------------------------------------
+pg_temp_statistic                    
+pg_temp_statistic_relid_att_inh_index
+(2 rows)
+
+count
+-----
+    0
+(1 row)
 
 step r1: ROLLBACK;
 step used1: 
   SELECT regexp_replace(oid::regclass::text, '_(\d+)', '_NNN', 'g')
     FROM pg_gtrs_in_use()
    ORDER BY 1;
+  SELECT count(*) FROM pg_temp_statistic;
 
-regexp_replace
---------------
-(0 rows)
+regexp_replace                       
+-------------------------------------
+pg_temp_statistic                    
+pg_temp_statistic_relid_att_inh_index
+(2 rows)
+
+count
+-----
+    0
+(1 row)
 
 
 starting permutation: ins1 idx1 sel1_idx ins2 sel2_idx
diff --git a/src/test/isolation/specs/global-temp.spec b/src/test/isolation/specs/global-temp.spec
index 56534154590..197662657b1 100644
--- a/src/test/isolation/specs/global-temp.spec
+++ b/src/test/isolation/specs/global-temp.spec
@@ -75,10 +75,12 @@ step get_tblspace1 {
 step reset_tblspace { ALTER TABLE tmp SET TABLESPACE pg_default; }
 
 # Test DROP from other backend
+step analyze1 { ANALYZE tmp2; }
 step used1 {
   SELECT regexp_replace(oid::regclass::text, '_(\d+)', '_NNN', 'g')
     FROM pg_gtrs_in_use()
    ORDER BY 1;
+  SELECT count(*) FROM pg_temp_statistic;
 }
 
 # Test val index
@@ -188,10 +190,10 @@ permutation ins1 ins2 alt_tblspace1 get_tblspace1 get_tblspace2
             sel1 sel2 reset_tblspace
 
 # Test DROP from other backend
-permutation create1 ins1_2 used1 drop1 used1
-permutation create1 ins1_2 used1 drop2 used1
-permutation create1 ins1_2 used1 b1 drop2 used1 r1 used1
-permutation create1 ins1_2 b1 used1 sp1 drop2 used1 rsp1 used1 r1 used1
+permutation create1 ins1_2 analyze1 used1 drop1 used1
+permutation create1 ins1_2 analyze1 used1 drop2 used1
+permutation create1 ins1_2 analyze1 used1 b1 drop2 used1 r1 used1
+permutation create1 ins1_2 analyze1 b1 used1 sp1 drop2 used1 rsp1 used1 r1 used1
 
 # Test val index
 permutation ins1 idx1 sel1_idx ins2 sel2_idx
diff --git a/src/test/recovery/t/018_wal_optimize.pl b/src/test/recovery/t/018_wal_optimize.pl
index 8f25b5dd165..8fd95980f36 100644
--- a/src/test/recovery/t/018_wal_optimize.pl
+++ b/src/test/recovery/t/018_wal_optimize.pl
@@ -29,6 +29,7 @@ sub check_orphan_relfilenodes
 		'postgres', "
 	   SELECT pg_relation_filepath(oid) FROM pg_class
 	   WHERE reltablespace = 0 AND relpersistence <> 't' AND
+       relpersistence <> 'g' AND
 	   pg_relation_filepath(oid) IS NOT NULL;");
 	is_deeply(
 		[
diff --git a/src/test/regress/expected/global_temp.out b/src/test/regress/expected/global_temp.out
index a1538146f4a..333ca88ce89 100644
--- a/src/test/regress/expected/global_temp.out
+++ b/src/test/regress/expected/global_temp.out
@@ -141,11 +141,13 @@ SELECT c.relname,
        age(t.relminmxid) <= age(c.relminmxid)
   FROM pg_gtrs_in_use() t LEFT JOIN pg_class c ON c.oid = t.oid
  ORDER BY c.relname;
-  relname  | ?column? | ?column? | ?column? | ?column? | ?column? | ?column? | ?column? | ?column? 
------------+----------+----------+----------+----------+----------+----------+----------+----------
- tmp1      | t        | t        | t        | t        | t        | t        | t        | t
- tmp1_pkey | t        | t        | t        | t        | t        | t        | t        | t
-(2 rows)
+                relname                | ?column? | ?column? | ?column? | ?column? | ?column? | ?column? | ?column? | ?column? 
+---------------------------------------+----------+----------+----------+----------+----------+----------+----------+----------
+ pg_temp_statistic                     | t        | t        | t        | t        | t        | t        | t        | t
+ pg_temp_statistic_relid_att_inh_index | t        | t        | t        | t        | t        | t        | t        | t
+ tmp1                                  | t        | t        | t        | t        | t        | t        | t        | t
+ tmp1_pkey                             | t        | t        | t        | t        | t        | t        | t        | t
+(4 rows)
 
 -- Test index
 INSERT INTO tmp1 VALUES (1, 'xxx');
@@ -611,12 +613,14 @@ SELECT c.relname,
   FROM pg_gtrs_in_use() t LEFT JOIN pg_class c ON c.oid = t.oid
  WHERE c.relname !~ 'pg_toast_'
  ORDER BY c.relname;
-  relname   | ?column? | ?column? 
-------------+----------+----------
- tmp1       | t        | t
- tmp1_c_seq | t        | t
- tmp1_pkey  | t        | t
-(3 rows)
+                relname                | ?column? | ?column? 
+---------------------------------------+----------+----------
+ pg_temp_statistic                     | t        | t
+ pg_temp_statistic_relid_att_inh_index | t        | t
+ tmp1                                  | t        | t
+ tmp1_c_seq                            | t        | t
+ tmp1_pkey                             | t        | t
+(5 rows)
 
 -- Test view creation
 CREATE VIEW v AS SELECT * FROM tmp1;
@@ -1109,3 +1113,40 @@ SELECT age(tempfrozenxid) - (SELECT max(age(t.relfrozenxid))
 (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;
diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out
index 51b9608a668..883ef6885c9 100644
--- a/src/test/regress/expected/oidjoins.out
+++ b/src/test/regress/expected/oidjoins.out
@@ -273,3 +273,14 @@ NOTICE:  checking pg_subscription {subowner} => pg_authid {oid}
 NOTICE:  checking pg_subscription {subserver} => pg_foreign_server {oid}
 NOTICE:  checking pg_subscription_rel {srsubid} => pg_subscription {oid}
 NOTICE:  checking pg_subscription_rel {srrelid} => pg_class {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 db6dce62a29..8998e5bf59c 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,
@@ -2706,7 +2771,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/expected/stats.out b/src/test/regress/expected/stats.out
index 8b15471248b..b19358821fa 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -81,6 +81,7 @@ standalone backend|relation|bulkwrite
 standalone backend|relation|init
 standalone backend|relation|normal
 standalone backend|relation|vacuum
+standalone backend|temp relation|normal
 standalone backend|wal|init
 standalone backend|wal|normal
 startup|relation|bulkread
@@ -104,7 +105,7 @@ walsummarizer|wal|init
 walsummarizer|wal|normal
 walwriter|wal|init
 walwriter|wal|normal
-(88 rows)
+(89 rows)
 \a
 -- List of registered statistics kinds.
 SELECT id, name, fixed_amount,
diff --git a/src/test/regress/sql/global_temp.sql b/src/test/regress/sql/global_temp.sql
index e0a15b66289..0813fadd915 100644
--- a/src/test/regress/sql/global_temp.sql
+++ b/src/test/regress/sql/global_temp.sql
@@ -595,3 +595,22 @@ SELECT age(tempfrozenxid) - (SELECT max(age(t.relfrozenxid))
  WHERE pid = pg_backend_pid();
 
 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;
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.51.0

