From 620c14e5937822e7f5f955021f9690ec296e7679 Mon Sep 17 00:00:00 2001
From: Dean Rasheed <dean.a.rasheed@gmail.com>
Date: Wed, 17 Jun 2026 02:04:59 +0100
Subject: [PATCH v10 06/11] Add relation statistics columns to pg_temp_class.

This adds relpages, reltuples, relallvisible, and relallfrozen columns
to pg_temp_class, and updates ANALYZE, CREATE INDEX, REPACK, VACUUM,
and pg_clear/restore_relation_stats() to apply statistics updates to
pg_temp_class instead of pg_class for global temporary relations. Each
session is then able to use its own local statistics when planning
queries.
---
 doc/src/sgml/catalogs.sgml                  |  87 +++++++-
 doc/src/sgml/func/func-admin.sgml           |   5 +-
 doc/src/sgml/indexam.sgml                   |   3 +-
 doc/src/sgml/maintenance.sgml               |  10 +-
 doc/src/sgml/monitoring.sgml                |  16 +-
 doc/src/sgml/perform.sgml                   |   5 +-
 doc/src/sgml/planstats.sgml                 |   6 +-
 src/backend/access/heap/heapam.c            |   4 +
 src/backend/catalog/catalog.c               |   2 +
 src/backend/catalog/heap.c                  |  41 ++--
 src/backend/catalog/index.c                 | 135 +++++++++----
 src/backend/catalog/pg_temp_class.c         |  28 +++
 src/backend/commands/repack.c               |  70 ++++---
 src/backend/commands/vacuum.c               |  87 +++++---
 src/backend/statistics/relation_stats.c     |  83 ++++----
 src/backend/utils/cache/gtcatcache.c        |  74 ++++++-
 src/backend/utils/cache/lsyscache.c         |  11 ++
 src/backend/utils/cache/relcache.c          |   9 +-
 src/include/catalog/pg_temp_class.h         | 165 ++++++++++++++++
 src/include/utils/gtcatcache.h              |   2 +
 src/include/utils/lsyscache.h               |   1 +
 src/test/isolation/expected/global-temp.out |  32 +++
 src/test/isolation/specs/global-temp.spec   |   6 +
 src/test/regress/expected/global_temp.out   | 208 ++++++++++++++++++++
 src/test/regress/sql/global_temp.sql        | 109 ++++++++++
 25 files changed, 1037 insertions(+), 162 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 910b98a2412..6b6789c3e1b 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2076,6 +2076,11 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <para>
        This is effectively an unsigned 32-bit integer; values larger than
        2<superscript>31</superscript>−1 are stored as negative values.
+      </para>
+      <para>
+       For a global temporary relation, the value from
+       <link linkend="catalog-pg-temp-class"><structname>pg_temp_class</structname></link>.<structfield>relpages</structfield>,
+       if any, takes precedence over the value from this catalog.
       </para></entry>
      </row>
 
@@ -2092,6 +2097,11 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
        analyzed, <structfield>reltuples</structfield>
        contains <literal>-1</literal> indicating that the row count is
        unknown.
+      </para>
+      <para>
+       For a global temporary relation, the value from
+       <link linkend="catalog-pg-temp-class"><structname>pg_temp_class</structname></link>.<structfield>reltuples</structfield>,
+       if any, takes precedence over the value from this catalog.
       </para></entry>
      </row>
 
@@ -2109,6 +2119,11 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <para>
        This is effectively an unsigned 32-bit integer; values larger than
        2<superscript>31</superscript>−1 are stored as negative values.
+      </para>
+      <para>
+       For a global temporary relation, the value from
+       <link linkend="catalog-pg-temp-class"><structname>pg_temp_class</structname></link>.<structfield>relallvisible</structfield>,
+       if any, takes precedence over the value from this catalog.
       </para></entry>
      </row>
 
@@ -2123,7 +2138,6 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
        scheduling manual vacuums and tuning <link
        linkend="runtime-config-vacuum-freezing">vacuum's freezing
        behavior</link>.
-
        It is updated by
        <link linkend="sql-vacuum"><command>VACUUM</command></link>,
        <link linkend="sql-analyze"><command>ANALYZE</command></link>,
@@ -2133,10 +2147,14 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <para>
        This is effectively an unsigned 32-bit integer; values larger than
        2<superscript>31</superscript>−1 are stored as negative values.
+      </para>
+      <para>
+       For a global temporary relation, the value from
+       <link linkend="catalog-pg-temp-class"><structname>pg_temp_class</structname></link>.<structfield>relallfrozen</structfield>,
+       if any, takes precedence over the value from this catalog.
       </para></entry>
      </row>
 
-
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>reltoastrelid</structfield> <type>oid</type>
@@ -9111,6 +9129,71 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
        specified in the creation command.
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>relpages</structfield> <type>int4</type>
+      </para>
+      <para>
+       Size of the on-disk representation of this table in the current
+       session in pages (of size <symbol>BLCKSZ</symbol>).
+       This is only an estimate used by the planner.  It is updated by
+       <link linkend="sql-vacuum"><command>VACUUM</command></link>,
+       <link linkend="sql-analyze"><command>ANALYZE</command></link>,
+       and a few DDL commands such as
+       <link linkend="sql-createindex"><command>CREATE INDEX</command></link>.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>reltuples</structfield> <type>float4</type>
+      </para>
+      <para>
+       Number of live rows in the table in the current session.  This is only
+       an estimate used by the planner.  It is updated by
+       <link linkend="sql-vacuum"><command>VACUUM</command></link>,
+       <link linkend="sql-analyze"><command>ANALYZE</command></link>,
+       and a few DDL commands such as
+       <link linkend="sql-createindex"><command>CREATE INDEX</command></link>.
+       If the table has never yet been vacuumed or analyzed in the current
+       session, <structfield>reltuples</structfield> contains
+       <literal>-1</literal> indicating that the row count is unknown.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>relallvisible</structfield> <type>int4</type>
+      </para>
+      <para>
+       Number of pages that are marked all-visible in the table's visibility
+       map for the current session.  This is only an estimate used by the
+       planner.  It is updated by
+       <link linkend="sql-vacuum"><command>VACUUM</command></link>,
+       <link linkend="sql-analyze"><command>ANALYZE</command></link>,
+       and a few DDL commands such as
+       <link linkend="sql-createindex"><command>CREATE INDEX</command></link>.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>relallfrozen</structfield> <type>int4</type>
+      </para>
+      <para>
+       Number of pages that are marked all-frozen in the table's visibility
+       map for the current session.  This is only an estimate that can be
+       used along with <structfield>relallvisible</structfield> for
+       scheduling manual vacuums and tuning
+       <link linkend="runtime-config-vacuum-freezing">vacuum's freezing behavior</link>.
+       It is updated by
+       <link linkend="sql-vacuum"><command>VACUUM</command></link>,
+       <link linkend="sql-analyze"><command>ANALYZE</command></link>,
+       and a few DDL commands such as
+       <link linkend="sql-createindex"><command>CREATE INDEX</command></link>.
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 2745179ba86..7ca6487fb2d 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -2026,7 +2026,10 @@ SELECT pg_restore_relation_stats(
          <literal>relname</literal> are required, and specify the table. Other
          arguments are the names and values of statistics corresponding to
          certain columns in <link
-         linkend="catalog-pg-class"><structname>pg_class</structname></link>.
+         linkend="catalog-pg-class"><structname>pg_class</structname></link>
+         (or <link
+         linkend="catalog-pg-temp-class"><structname>pg_temp_class</structname></link>
+         for a global temporary table).
          The currently-supported relation statistics are
          <literal>relpages</literal> with a value of type
          <type>integer</type>, <literal>reltuples</literal> with a value of
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index 6649bdd6cd1..3298717ace5 100644
--- a/doc/src/sgml/indexam.sgml
+++ b/doc/src/sgml/indexam.sgml
@@ -450,7 +450,8 @@ amvacuumcleanup (IndexVacuumInfo *info,
    last <function>ambulkdelete</function> call returned, or NULL if
    <function>ambulkdelete</function> was not called because no tuples needed to be
    deleted.  If the result is not NULL it must be a palloc'd struct.
-   The statistics it contains will be used to update <structname>pg_class</structname>,
+   The statistics it contains will be used to update <structname>pg_class</structname>
+   (or <structname>pg_temp_class</structname> for an index on a global temporary table),
    and will be reported by <command>VACUUM</command> if <literal>VERBOSE</literal> is given.
    It is OK to return NULL if the index was not changed at all during the
    <command>VACUUM</command> operation, but otherwise correct stats should
diff --git a/doc/src/sgml/maintenance.sgml b/doc/src/sgml/maintenance.sgml
index ac2fa900ba7..84c5df9095b 100644
--- a/doc/src/sgml/maintenance.sgml
+++ b/doc/src/sgml/maintenance.sgml
@@ -973,7 +973,9 @@ vacuum threshold = Minimum(vacuum max threshold, vacuum base threshold + vacuum
     the vacuum scale factor is
     <xref linkend="guc-autovacuum-vacuum-scale-factor"/>,
     and the number of tuples is
-    <structname>pg_class</structname>.<structfield>reltuples</structfield>.
+    <structname>pg_class</structname>.<structfield>reltuples</structfield> (or
+    <structname>pg_temp_class</structname>.<structfield>reltuples</structfield>
+    for a global temporary table).
    </para>
 
    <para>
@@ -987,9 +989,11 @@ vacuum insert threshold = vacuum base insert threshold + vacuum insert scale fac
     the vacuum insert scale factor is
     <xref linkend="guc-autovacuum-vacuum-insert-scale-factor"/>,
     the number of tuples is
-    <structname>pg_class</structname>.<structfield>reltuples</structfield>,
+    <structname>pg_class</structname>.<structfield>reltuples</structfield> or
+    <structname>pg_temp_class</structname>.<structfield>reltuples</structfield>,
     and the percent of the table not frozen is
-    <literal>1 - pg_class.relallfrozen / pg_class.relpages</literal>.
+    <literal>1 - pg_class.relallfrozen / pg_class.relpages</literal> or
+    <literal>1 - pg_temp_class.relallfrozen / pg_temp_class.relpages</literal>.
     Such vacuums may allow portions of the table to be marked as
     <firstterm>all visible</firstterm> and also allow tuples to be frozen, which
     can reduce the work required in subsequent vacuums.
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index b403fb990a7..e1e21bf89cb 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -6691,7 +6691,8 @@ FROM pg_stat_get_backend_idset() AS backendid;
      <row>
       <entry><literal>finalizing analyze</literal></entry>
       <entry>
-       The command is updating <structname>pg_class</structname>. When this
+       The command is updating <structname>pg_class</structname> or
+       <structname>pg_temp_class</structname>. When this
        phase is completed, <command>ANALYZE</command> will end.
       </entry>
      </row>
@@ -7986,7 +7987,8 @@ FROM pg_stat_get_backend_idset() AS backendid;
      <entry>
        <command>VACUUM</command> is performing final cleanup.  During this phase,
        <command>VACUUM</command> will vacuum the free space map, update statistics
-       in <literal>pg_class</literal>, and report statistics to the cumulative
+       in <literal>pg_class</literal> or <literal>pg_temp_class</literal>,
+       and report statistics to the cumulative
        statistics system. When this phase is completed, <command>VACUUM</command> will end.
      </entry>
     </row>
@@ -9187,6 +9189,16 @@ SELECT pg_relation_filepath(oid), relpages FROM pg_class WHERE relname = 'custom
 ----------------------+----------
  base/16384/16806     |       60
 (1 row)
+</programlisting>
+    But for a global temporary table, you should use the statistical information
+from <structname>pg_temp_class</structname> instead of <structname>pg_class</structname>:
+<programlisting>
+SELECT pg_relation_filepath(oid), relpages FROM pg_temp_class WHERE oid = 'customer'::regclass;
+
+ pg_relation_filepath | relpages
+----------------------+----------
+ base/16384/t4_16398  |       46
+(1 row)
 </programlisting>
     Each page is typically 8 kilobytes. (Remember, <structfield>relpages</structfield>
     is only updated by <command>VACUUM</command>, <command>ANALYZE</command>, and
diff --git a/doc/src/sgml/perform.sgml b/doc/src/sgml/perform.sgml
index ea8da01b779..80a6509bb25 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -1281,7 +1281,9 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique2 &gt; 9000
    One component of the statistics is the total number of entries in
    each table and index, as well as the number of disk blocks occupied
    by each table and index.  This information is kept in the table
-   <link linkend="catalog-pg-class"><structname>pg_class</structname></link>,
+   <link linkend="catalog-pg-class"><structname>pg_class</structname></link>
+   (or <link linkend="catalog-pg-temp-class"><structname>pg_temp_class</structname></link>
+   for global temporary tables and indexes),
    in the columns <structfield>reltuples</structfield> and
    <structfield>relpages</structfield>.  We can look at it with
    queries similar to this one:
@@ -1318,6 +1320,7 @@ WHERE relname LIKE 'tenk1%';
    of the table it did scan, resulting in an approximate value.
    In any case, the planner
    will scale the values it finds in <structname>pg_class</structname>
+   and <structname>pg_temp_class</structname>
    to match the current physical table size, thus obtaining a closer
    approximation.
   </para>
diff --git a/doc/src/sgml/planstats.sgml b/doc/src/sgml/planstats.sgml
index e57867ba617..72f242f2cb3 100644
--- a/doc/src/sgml/planstats.sgml
+++ b/doc/src/sgml/planstats.sgml
@@ -49,7 +49,8 @@ EXPLAIN SELECT * FROM tenk1;
    How the planner determines the cardinality of <structname>tenk1</structname>
    is covered in <xref linkend="planner-stats"/>, but is repeated here for
    completeness. The number of pages and rows is looked up in
-   <structname>pg_class</structname>:
+   <structname>pg_class</structname> (or <structname>pg_temp_class</structname>
+   for a global temporary table):
 
 <programlisting>
 SELECT relpages, reltuples FROM pg_class WHERE relname = 'tenk1';
@@ -470,7 +471,8 @@ ANALYZE t;
 
     As explained in <xref linkend="planner-stats"/>, the planner can determine
     cardinality of <structname>t</structname> using the number of pages and
-    rows obtained from <structname>pg_class</structname>:
+    rows obtained from <structname>pg_class</structname> (or
+    <structname>pg_temp_class</structname> for a global temporary table):
 
 <programlisting>
 SELECT relpages, reltuples FROM pg_class WHERE relname = 't';
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 72d6541734c..ef4569d24a0 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -42,6 +42,7 @@
 #include "access/xloginsert.h"
 #include "catalog/pg_database.h"
 #include "catalog/pg_database_d.h"
+#include "catalog/pg_temp_class.h"
 #include "commands/vacuum.h"
 #include "executor/instrument_node.h"
 #include "pgstat.h"
@@ -4395,6 +4396,9 @@ check_lock_if_inplace_updateable_rel(Relation relation,
 					return;
 			}
 			break;
+		case TempRelationRelationId:
+			/* No lock required -- temp tables are only accessible by us */
+			return;
 		default:
 			Assert(!IsInplaceUpdateRelation(relation));
 			return;
diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c
index d99bb724b83..eb5d849c067 100644
--- a/src/backend/catalog/catalog.c
+++ b/src/backend/catalog/catalog.c
@@ -40,6 +40,7 @@
 #include "catalog/pg_shseclabel.h"
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_tablespace.h"
+#include "catalog/pg_temp_class.h"
 #include "catalog/pg_type.h"
 #include "miscadmin.h"
 #include "utils/fmgroids.h"
@@ -195,6 +196,7 @@ bool
 IsInplaceUpdateOid(Oid relid)
 {
 	return (relid == RelationRelationId ||
+			relid == TempRelationRelationId ||
 			relid == DatabaseRelationId);
 }
 
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 75cd7e09fd1..5d32935d20e 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -3595,10 +3595,10 @@ RemoveStatistics(Oid relid, AttrNumber attnum)
  * with the heap relation to zero tuples.
  *
  * The routine will truncate and then reconstruct the indexes on
- * the specified relation.  Caller must hold exclusive lock on rel.
+ * the specified relation.  Caller must hold the specified lock on rel.
  */
 static void
-RelationTruncateIndexes(Relation heapRelation)
+RelationTruncateIndexes(Relation heapRelation, LOCKMODE lockmode)
 {
 	ListCell   *indlist;
 
@@ -3609,8 +3609,8 @@ RelationTruncateIndexes(Relation heapRelation)
 		Relation	currentIndex;
 		IndexInfo  *indexInfo;
 
-		/* Open the index relation; use exclusive lock, just to be sure */
-		currentIndex = index_open(indexId, AccessExclusiveLock);
+		/* Open the index relation; use same lock as heap relation */
+		currentIndex = index_open(indexId, lockmode);
 
 		/*
 		 * Fetch info needed for index_build.  Since we know there are no
@@ -3652,13 +3652,23 @@ heap_truncate(List *relids)
 	List	   *relations = NIL;
 	ListCell   *cell;
 
-	/* Open relations for processing, and grab exclusive access on each */
+	/*
+	 * Open relations for processing.  For most relations, we must use
+	 * AccessExclusiveLock to prevent schema and data changes.  However, for
+	 * global temporary relations, we must use RowExclusiveLock, because two
+	 * backends trying to upgrade to an exclusive lock on the same relation
+	 * here would deadlock.  This is sufficient, because the relation's data
+	 * is session-local.
+	 */
 	foreach(cell, relids)
 	{
 		Oid			rid = lfirst_oid(cell);
+		LOCKMODE	lockmode;
 		Relation	rel;
 
-		rel = table_open(rid, AccessExclusiveLock);
+		lockmode = rel_is_global_temp(rid) ? RowExclusiveLock : AccessExclusiveLock;
+
+		rel = table_open(rid, lockmode);
 		relations = lappend(relations, rel);
 	}
 
@@ -3673,7 +3683,7 @@ heap_truncate(List *relids)
 		/* Truncate the relation */
 		heap_truncate_one_rel(rel);
 
-		/* Close the relation, but keep exclusive lock on it until commit */
+		/* Close the relation, but keep lock on it until commit */
 		table_close(rel, NoLock);
 	}
 }
@@ -3685,13 +3695,22 @@ heap_truncate(List *relids)
  *
  * This is not transaction-safe, because the truncation is done immediately
  * and cannot be rolled back later.  Caller is responsible for having
- * checked permissions etc, and must have obtained AccessExclusiveLock.
+ * checked permissions etc, and must have obtained the required lock, which is
+ * typically AccessExclusiveLock, except if it's a global temporary relation,
+ * in which case RowExclusiveLock is sufficient.
  */
 void
 heap_truncate_one_rel(Relation rel)
 {
+	LOCKMODE	lockmode;
 	Oid			toastrelid;
 
+	/*
+	 * For a global temporary relation, RowExclusiveLock is sufficient.
+	 * Otherwise must use AccessExclusiveLock.
+	 */
+	lockmode = RELATION_IS_GLOBAL_TEMP(rel) ? RowExclusiveLock : AccessExclusiveLock;
+
 	/*
 	 * Truncate the relation.  Partitioned tables have no storage, so there is
 	 * nothing to do for them here.
@@ -3703,16 +3722,16 @@ heap_truncate_one_rel(Relation rel)
 	table_relation_nontransactional_truncate(rel);
 
 	/* If the relation has indexes, truncate the indexes too */
-	RelationTruncateIndexes(rel);
+	RelationTruncateIndexes(rel, lockmode);
 
 	/* If there is a toast table, truncate that too */
 	toastrelid = rel->rd_rel->reltoastrelid;
 	if (OidIsValid(toastrelid))
 	{
-		Relation	toastrel = table_open(toastrelid, AccessExclusiveLock);
+		Relation	toastrel = table_open(toastrelid, lockmode);
 
 		table_relation_nontransactional_truncate(toastrel);
-		RelationTruncateIndexes(toastrel);
+		RelationTruncateIndexes(toastrel, lockmode);
 		/* keep the lock... */
 		table_close(toastrel, NoLock);
 	}
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 63abce22fe2..127fafd81f5 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -123,6 +123,7 @@ static void UpdateIndexRelation(Oid indexoid, Oid heapoid,
 								bool isvalid,
 								bool isready);
 static void index_update_stats(Relation rel,
+							   bool isreindex,
 							   bool hasindex,
 							   double reltuples);
 static void IndexCheckExclusion(Relation heapRelation,
@@ -1282,6 +1283,7 @@ index_create(Relation heapRelation,
 		 * having an index.
 		 */
 		index_update_stats(heapRelation,
+						   false,
 						   true,
 						   -1.0);
 		/* Make the above update visible */
@@ -2833,28 +2835,37 @@ FormIndexDatum(IndexInfo *indexInfo,
 
 
 /*
- * index_update_stats --- update pg_class entry after CREATE INDEX or REINDEX
+ * index_update_stats --- update effective pg_class entry after CREATE INDEX
+ * or REINDEX
  *
- * This routine updates the pg_class row of either an index or its parent
- * relation after CREATE INDEX or REINDEX.  Its rather bizarre API is designed
- * to ensure we can do all the necessary work in just one update.
+ * This routine updates the effective pg_class row of either an index or its
+ * parent relation after CREATE INDEX or REINDEX.  Its rather bizarre API is
+ * designed to ensure we can do all the necessary work in just one update
+ * (except for a global temporary relation, which requires both pg_class and
+ * pg_temp_class to be updated).
  *
+ * isreindex: recreated a previously-existing index
  * hasindex: set relhasindex to this value
  * reltuples: if >= 0, set reltuples to this value; else no change
  *
  * If reltuples >= 0, relpages, relallvisible, and relallfrozen are also
  * updated (using RelationGetNumberOfBlocks() and visibilitymap_count()).
  *
+ * For a new index on a global temporary relation, relhasindex is set in
+ * pg_class and all the other fields are set in pg_temp_class.  For any other
+ * type of relation, all the fields are set in pg_class.
+ *
  * NOTE: an important side-effect of this operation is that an SI invalidation
  * message is sent out to all backends --- including me --- causing relcache
  * entries to be flushed or updated with the new data.  This must happen even
- * if we find that no change is needed in the pg_class row.  When updating
- * a heap entry, this ensures that other backends find out about the new
- * index.  When updating an index, it's important because some index AMs
- * expect a relcache flush to occur after REINDEX.
+ * if we find that no change is needed in the pg_class or pg_temp_class rows.
+ * When updating a heap entry, this ensures that other backends find out about
+ * the new index.  When updating an index, it's important because some index
+ * AMs expect a relcache flush to occur after REINDEX.
  */
 static void
 index_update_stats(Relation rel,
+				   bool isreindex,
 				   bool hasindex,
 				   double reltuples)
 {
@@ -2866,9 +2877,12 @@ index_update_stats(Relation rel,
 	Relation	pg_class;
 	ScanKeyData key[1];
 	HeapTuple	tuple;
+	HeapTuple	temp_tuple;
 	void	   *state;
 	Form_pg_class rd_rel;
+	Form_pg_temp_class temp_rd_rel;
 	bool		dirty;
+	bool		temp_dirty;
 
 	/*
 	 * As a special hack, if we are dealing with an empty table and the
@@ -2952,16 +2966,54 @@ index_update_stats(Relation rel,
 	 * relallvisible) if the caller isn't providing an updated reltuples
 	 * count, because that would bollix the reltuples/relpages ratio which is
 	 * what's really important.
+	 *
+	 * If not for (1) above, pg_temp_class could be updated normally, and in
+	 * fact we could work round (1) by simply not updating pg_temp_class in
+	 * bootstrap mode, since its value just gets thrown away when initdb
+	 * finishes.  However, for consistency, we update it in-place, like
+	 * pg_class --- see also vac_update_relstats().
 	 */
 
-	pg_class = table_open(RelationRelationId, RowExclusiveLock);
+	/*
+	 * For a global temporary relation, need a copy of its pg_temp_class
+	 * tuple.
+	 */
+	if (RELATION_IS_GLOBAL_TEMP(rel))
+	{
+		temp_tuple = GetPgTempClassTuple(relid);
+		if (!HeapTupleIsValid(temp_tuple))
+			elog(ERROR, "cache lookup failed for global temp relation %u", relid);
+		temp_rd_rel = (Form_pg_temp_class) GETSTRUCT(temp_tuple);
+	}
+	else
+	{
+		temp_tuple = NULL;
+		temp_rd_rel = NULL;
+	}
 
-	ScanKeyInit(&key[0],
-				Anum_pg_class_oid,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(relid));
-	systable_inplace_update_begin(pg_class, ClassOidIndexId, true, NULL,
-								  1, key, &tuple, &state);
+	/*
+	 * If this is a reindex on a global temporary table, we don't need to set
+	 * pg_class.relhasindex, and all other fields go in pg_temp_class, so we
+	 * only need a read-only copy of the pg_class tuple.  Otherwise, we need a
+	 * writable copy of the pg_class tuple to scribble on.
+	 */
+	if (isreindex && RELATION_IS_GLOBAL_TEMP(rel))
+	{
+		pg_class = NULL;
+		tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+		state = NULL;
+	}
+	else
+	{
+		pg_class = table_open(RelationRelationId, RowExclusiveLock);
+
+		ScanKeyInit(&key[0],
+					Anum_pg_class_oid,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(relid));
+		systable_inplace_update_begin(pg_class, ClassOidIndexId, true, NULL,
+									  1, key, &tuple, &state);
+	}
 
 	if (!HeapTupleIsValid(tuple))
 		elog(ERROR, "could not find tuple for relation %u", relid);
@@ -2970,10 +3022,11 @@ index_update_stats(Relation rel,
 	/* Should this be a more comprehensive test? */
 	Assert(rd_rel->relkind != RELKIND_PARTITIONED_INDEX);
 
-	/* Apply required updates, if any, to copied tuple */
+	/* Apply required updates, if any, to copied tuple(s) */
 
 	dirty = false;
-	if (rd_rel->relhasindex != hasindex)
+	temp_dirty = false;
+	if (RelationIsValid(pg_class) && rd_rel->relhasindex != hasindex)
 	{
 		rd_rel->relhasindex = hasindex;
 		dirty = true;
@@ -2981,30 +3034,18 @@ index_update_stats(Relation rel,
 
 	if (update_stats)
 	{
-		if (rd_rel->relpages != (int32) relpages)
-		{
-			rd_rel->relpages = (int32) relpages;
-			dirty = true;
-		}
-		if (rd_rel->reltuples != (float4) reltuples)
-		{
-			rd_rel->reltuples = (float4) reltuples;
-			dirty = true;
-		}
-		if (rd_rel->relallvisible != (int32) relallvisible)
-		{
-			rd_rel->relallvisible = (int32) relallvisible;
-			dirty = true;
-		}
-		if (rd_rel->relallfrozen != (int32) relallfrozen)
-		{
-			rd_rel->relallfrozen = (int32) relallfrozen;
-			dirty = true;
-		}
+		SetEffective_relpages(rd_rel, temp_rd_rel, (int32) relpages,
+							  &dirty, &temp_dirty);
+		SetEffective_reltuples(rd_rel, temp_rd_rel, (float4) reltuples,
+							   &dirty, &temp_dirty);
+		SetEffective_relallvisible(rd_rel, temp_rd_rel, (int32) relallvisible,
+								   &dirty, &temp_dirty);
+		SetEffective_relallfrozen(rd_rel, temp_rd_rel, (int32) relallfrozen,
+								  &dirty, &temp_dirty);
 	}
 
 	/*
-	 * If anything changed, write out the tuple
+	 * If anything changed, write out the tuple(s)
 	 */
 	if (dirty)
 	{
@@ -3013,7 +3054,8 @@ index_update_stats(Relation rel,
 	}
 	else
 	{
-		systable_inplace_update_cancel(state);
+		if (state != NULL)
+			systable_inplace_update_cancel(state);
 
 		/*
 		 * While we didn't change relhasindex, CREATE INDEX needs a
@@ -3025,9 +3067,18 @@ index_update_stats(Relation rel,
 		CacheInvalidateRelcacheByTuple(tuple);
 	}
 
+	if (HeapTupleIsValid(temp_tuple))
+	{
+		if (temp_dirty)
+			UpdatePgTempClassTupleInPlace(relid, temp_tuple);
+
+		heap_freetuple(temp_tuple);
+	}
+
 	heap_freetuple(tuple);
 
-	table_close(pg_class, RowExclusiveLock);
+	if (RelationIsValid(pg_class))
+		table_close(pg_class, RowExclusiveLock);
 }
 
 
@@ -3205,11 +3256,11 @@ index_build(Relation heapRelation,
 	 * Update heap and index pg_class rows
 	 */
 	index_update_stats(heapRelation,
-					   true,
+					   isreindex, true,
 					   stats->heap_tuples);
 
 	index_update_stats(indexRelation,
-					   false,
+					   isreindex, false,
 					   stats->index_tuples);
 
 	/* Make the updated catalog row versions visible */
diff --git a/src/backend/catalog/pg_temp_class.c b/src/backend/catalog/pg_temp_class.c
index d01b5a02162..af225541c1c 100644
--- a/src/backend/catalog/pg_temp_class.c
+++ b/src/backend/catalog/pg_temp_class.c
@@ -62,6 +62,18 @@ get_pg_temp_class_tupdesc(void)
 		TupleDescInitEntry(tupdesc,
 						   (AttrNumber) Anum_pg_temp_class_reltablespace,
 						   "reltablespace", OIDOID, -1, 0);
+		TupleDescInitEntry(tupdesc,
+						   (AttrNumber) Anum_pg_temp_class_relpages,
+						   "relpages", INT4OID, -1, 0);
+		TupleDescInitEntry(tupdesc,
+						   (AttrNumber) Anum_pg_temp_class_reltuples,
+						   "reltuples", FLOAT4OID, -1, 0);
+		TupleDescInitEntry(tupdesc,
+						   (AttrNumber) Anum_pg_temp_class_relallvisible,
+						   "relallvisible", INT4OID, -1, 0);
+		TupleDescInitEntry(tupdesc,
+						   (AttrNumber) Anum_pg_temp_class_relallfrozen,
+						   "relallfrozen", INT4OID, -1, 0);
 		TupleDescFinalize(tupdesc);
 
 		MemoryContextSwitchTo(oldcontext);
@@ -120,6 +132,10 @@ InsertPgTempClassTuple(Relation rel)
 	values[Anum_pg_temp_class_oid - 1] = ObjectIdGetDatum(RelationGetRelid(rel));
 	values[Anum_pg_temp_class_relfilenode - 1] = ObjectIdGetDatum(form->relfilenode);
 	values[Anum_pg_temp_class_reltablespace - 1] = ObjectIdGetDatum(form->reltablespace);
+	values[Anum_pg_temp_class_relpages - 1] = Int32GetDatum(form->relpages);
+	values[Anum_pg_temp_class_reltuples - 1] = Float4GetDatum(form->reltuples);
+	values[Anum_pg_temp_class_relallvisible - 1] = Int32GetDatum(form->relallvisible);
+	values[Anum_pg_temp_class_relallfrozen - 1] = Int32GetDatum(form->relallfrozen);
 
 	GTCatCacheTupleInsert(PG_TEMP_CLASS,
 						  RelationGetRelid(rel),
@@ -139,6 +155,18 @@ UpdatePgTempClassTuple(Oid relid, HeapTuple newtuple)
 	GTCatCacheTupleUpdate(PG_TEMP_CLASS, relid, newtuple);
 }
 
+/*
+ * UpdatePgTempClassTupleInPlace
+ *
+ *	Do an in-place update of the pg_temp_class tuple for a global temporary
+ *	relation.
+ */
+void
+UpdatePgTempClassTupleInPlace(Oid relid, HeapTuple newtuple)
+{
+	GTCatCacheTupleUpdateInPlace(PG_TEMP_CLASS, relid, newtuple);
+}
+
 /*
  * DeletePgTempClassTuple
  *
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 89ae4fc3b8a..2deeb523848 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -1266,7 +1266,9 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 {
 	Relation	relRelation;
 	HeapTuple	reltup;
+	HeapTuple	temp_reltup;
 	Form_pg_class relform;
+	Form_pg_temp_class temp_relform;
 	TupleDesc	oldTupDesc PG_USED_FOR_ASSERTS_ONLY;
 	TupleDesc	newTupDesc PG_USED_FOR_ASSERTS_ONLY;
 	VacuumParams params;
@@ -1458,24 +1460,36 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 					   tups_recently_dead,
 					   pg_rusage_show(&ru0))));
 
-	/* Update pg_class to reflect the correct values of pages and tuples. */
+	/*
+	 * Update pg_class / pg_temp_class to reflect the correct values of pages
+	 * and tuples.
+	 */
 	relRelation = table_open(RelationRelationId, RowExclusiveLock);
 
-	reltup = SearchSysCacheCopy1(RELOID,
-								 ObjectIdGetDatum(RelationGetRelid(NewHeap)));
+	reltup = GetPgClassAndPgTempClassTuples(RelationGetRelid(NewHeap), false,
+											&temp_reltup, true);
 	if (!HeapTupleIsValid(reltup))
 		elog(ERROR, "cache lookup failed for relation %u",
 			 RelationGetRelid(NewHeap));
 	relform = (Form_pg_class) GETSTRUCT(reltup);
+	temp_relform = (Form_pg_temp_class) GETSTRUCT_SAFE(temp_reltup);
 
-	relform->relpages = num_pages;
-	relform->reltuples = num_tuples;
+	SetEffective_relpages(relform, temp_relform, num_pages, NULL, NULL);
+	SetEffective_reltuples(relform, temp_relform, num_tuples, NULL, NULL);
 
-	/* Don't update the stats for pg_class.  See swap_relation_files. */
-	if (RelationGetRelid(OldHeap) != RelationRelationId)
-		CatalogTupleUpdate(relRelation, &reltup->t_self, reltup);
+	if (HeapTupleIsValid(temp_reltup))
+	{
+		UpdatePgTempClassTuple(RelationGetRelid(NewHeap), temp_reltup);
+		heap_freetuple(temp_reltup);
+	}
 	else
-		CacheInvalidateRelcacheByTuple(reltup);
+	{
+		/* Don't update the stats for pg_class.  See swap_relation_files. */
+		if (RelationGetRelid(OldHeap) != RelationRelationId)
+			CatalogTupleUpdate(relRelation, &reltup->t_self, reltup);
+		else
+			CacheInvalidateRelcacheByTuple(reltup);
+	}
 
 	/* Clean up. */
 	heap_freetuple(reltup);
@@ -1702,21 +1716,29 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class,
 		int32		swap_allvisible;
 		int32		swap_allfrozen;
 
-		swap_pages = relform1->relpages;
-		relform1->relpages = relform2->relpages;
-		relform2->relpages = swap_pages;
-
-		swap_tuples = relform1->reltuples;
-		relform1->reltuples = relform2->reltuples;
-		relform2->reltuples = swap_tuples;
-
-		swap_allvisible = relform1->relallvisible;
-		relform1->relallvisible = relform2->relallvisible;
-		relform2->relallvisible = swap_allvisible;
-
-		swap_allfrozen = relform1->relallfrozen;
-		relform1->relallfrozen = relform2->relallfrozen;
-		relform2->relallfrozen = swap_allfrozen;
+		swap_pages = GetEffective_relpages(relform1, temp_relform1);
+		SetEffective_relpages(relform1, temp_relform1,
+							  GetEffective_relpages(relform2, temp_relform2),
+							  NULL, NULL);
+		SetEffective_relpages(relform2, temp_relform2, swap_pages, NULL, NULL);
+
+		swap_tuples = GetEffective_reltuples(relform1, temp_relform1);
+		SetEffective_reltuples(relform1, temp_relform1,
+							   GetEffective_reltuples(relform2, temp_relform2),
+							   NULL, NULL);
+		SetEffective_reltuples(relform2, temp_relform2, swap_tuples, NULL, NULL);
+
+		swap_allvisible = GetEffective_relallvisible(relform1, temp_relform1);
+		SetEffective_relallvisible(relform1, temp_relform1,
+								   GetEffective_relallvisible(relform2, temp_relform2),
+								   NULL, NULL);
+		SetEffective_relallvisible(relform2, temp_relform2, swap_allvisible, NULL, NULL);
+
+		swap_allfrozen = GetEffective_relallfrozen(relform1, temp_relform1);
+		SetEffective_relallfrozen(relform1, temp_relform1,
+								  GetEffective_relallfrozen(relform2, temp_relform2),
+								  NULL, NULL);
+		SetEffective_relallfrozen(relform2, temp_relform2, swap_allfrozen, NULL, NULL);
 	}
 
 	/*
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 03fe9dba839..1defddbf0e7 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -1419,9 +1419,10 @@ vac_estimate_reltuples(Relation relation,
  *	vac_update_relstats() -- update statistics for one relation
  *
  *		Update the whole-relation statistics that are kept in its pg_class
- *		row.  There are additional stats that will be updated if we are
- *		doing ANALYZE, but we always update these stats.  This routine works
- *		for both index and heap relation entries in pg_class.
+ *		row (and its pg_temp_class row, for a global temporary relation).
+ *		There are additional stats that will be updated if we are doing
+ *		ANALYZE, but we always update these stats. This routine works for both
+ *		index and heap relation entries in pg_class and pg_temp_class.
  *
  *		We violate transaction semantics here by overwriting the rel's
  *		existing pg_class tuple with the new values.  This is reasonably
@@ -1430,12 +1431,17 @@ vac_estimate_reltuples(Relation relation,
  *		we updated these tuples in the usual way, vacuuming pg_class itself
  *		wouldn't work very well --- by the time we got done with a vacuum
  *		cycle, most of the tuples in pg_class would've been obsoleted.  Of
- *		course, this only works for fixed-size not-null columns, but these are.
+ *		course, this only works for fixed-size not-null columns, but these
+ *		are.  Likewise for pg_temp_class, which is also updated for a global
+ *		temporary relation.
  *
  *		Another reason for doing it this way is that when we are in a lazy
  *		VACUUM and have PROC_IN_VACUUM set, we mustn't do any regular updates.
  *		Somebody vacuuming pg_class might think they could delete a tuple
- *		marked with xmin = our xid.
+ *		marked with xmin = our xid.  This isn't a problem for pg_temp_class
+ *		because no other session can see our copy of its data, but it still
+ *		makes sense to do an in-place update to avoid vacuumed pg_temp_class
+ *		tuples being obsoleted.
  *
  *		In addition to fundamentally nontransactional statistics such as
  *		relpages and relallvisible, we try to maintain certain lazily-updated
@@ -1450,8 +1456,8 @@ vac_estimate_reltuples(Relation relation,
  *		transaction.  This is OK since postponing the flag maintenance is
  *		always allowable.
  *
- *		Note: num_tuples should count only *live* tuples, since
- *		pg_class.reltuples is defined that way.
+ *		Note: num_tuples should count only *live* tuples, since reltuples in
+ *		pg_class and pg_temp_class is defined that way.
  *
  *		This routine is shared by VACUUM and ANALYZE.
  */
@@ -1469,17 +1475,37 @@ vac_update_relstats(Relation relation,
 	Relation	rd;
 	ScanKeyData key[1];
 	HeapTuple	ctup;
+	HeapTuple	temp_ctup;
 	void	   *inplace_state;
 	Form_pg_class pgcform;
+	Form_pg_temp_class temp_pgcform;
 	bool		dirty,
+				temp_dirty,
 				futurexid,
 				futuremxid;
 	TransactionId oldfrozenxid;
 	MultiXactId oldminmulti;
 
+	/*
+	 * For a global temporary relation, need a copy of its pg_temp_class
+	 * tuple.
+	 */
+	if (RELATION_IS_GLOBAL_TEMP(relation))
+	{
+		temp_ctup = GetPgTempClassTuple(relid);
+		if (!HeapTupleIsValid(temp_ctup))
+			elog(ERROR, "cache lookup failed for global temp relation %u", relid);
+		temp_pgcform = (Form_pg_temp_class) GETSTRUCT(temp_ctup);
+	}
+	else
+	{
+		temp_ctup = NULL;
+		temp_pgcform = NULL;
+	}
+
+	/* Fetch a copy of the pg_class tuple to scribble on */
 	rd = table_open(RelationRelationId, RowExclusiveLock);
 
-	/* Fetch a copy of the tuple to scribble on */
 	ScanKeyInit(&key[0],
 				Anum_pg_class_oid,
 				BTEqualStrategyNumber, F_OIDEQ,
@@ -1491,29 +1517,20 @@ vac_update_relstats(Relation relation,
 			 relid);
 	pgcform = (Form_pg_class) GETSTRUCT(ctup);
 
-	/* Apply statistical updates, if any, to copied tuple */
+	/* Apply statistical updates, if any, to copied tuple(s) */
 
 	dirty = false;
-	if (pgcform->relpages != (int32) num_pages)
-	{
-		pgcform->relpages = (int32) num_pages;
-		dirty = true;
-	}
-	if (pgcform->reltuples != (float4) num_tuples)
-	{
-		pgcform->reltuples = (float4) num_tuples;
-		dirty = true;
-	}
-	if (pgcform->relallvisible != (int32) num_all_visible_pages)
-	{
-		pgcform->relallvisible = (int32) num_all_visible_pages;
-		dirty = true;
-	}
-	if (pgcform->relallfrozen != (int32) num_all_frozen_pages)
-	{
-		pgcform->relallfrozen = (int32) num_all_frozen_pages;
-		dirty = true;
-	}
+	temp_dirty = false;
+	SetEffective_relpages(pgcform, temp_pgcform, (int32) num_pages,
+						  &dirty, &temp_dirty);
+	SetEffective_reltuples(pgcform, temp_pgcform, (float4) num_tuples,
+						   &dirty, &temp_dirty);
+	SetEffective_relallvisible(pgcform, temp_pgcform,
+							   (int32) num_all_visible_pages,
+							   &dirty, &temp_dirty);
+	SetEffective_relallfrozen(pgcform, temp_pgcform,
+							  (int32) num_all_frozen_pages,
+							  &dirty, &temp_dirty);
 
 	/* Apply DDL updates, but not inside an outer transaction (see above) */
 
@@ -1596,12 +1613,22 @@ vac_update_relstats(Relation relation,
 		}
 	}
 
-	/* If anything changed, write out the tuple. */
+	/* If anything changed, write out the tuple(s) */
 	if (dirty)
 		systable_inplace_update_finish(inplace_state, ctup);
 	else
 		systable_inplace_update_cancel(inplace_state);
 
+	if (HeapTupleIsValid(temp_ctup))
+	{
+		if (temp_dirty)
+			UpdatePgTempClassTupleInPlace(relid, temp_ctup);
+
+		heap_freetuple(temp_ctup);
+	}
+
+	heap_freetuple(ctup);
+
 	table_close(rd, RowExclusiveLock);
 
 	if (futurexid)
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index 28d87fa6f77..1d8d8050f4f 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -22,6 +22,7 @@
 #include "access/heapam.h"
 #include "catalog/indexing.h"
 #include "catalog/namespace.h"
+#include "catalog/pg_temp_class.h"
 #include "nodes/makefuncs.h"
 #include "statistics/statistics.h"
 #include "statistics/stat_utils.h"
@@ -120,10 +121,10 @@ relation_statistics_update_internal(Oid reloid,
 	Relation	crel;
 	HeapTuple	ctup;
 	Form_pg_class pgcform;
-	int			replaces[4] = {0};
-	Datum		values[4] = {0};
-	bool		nulls[4] = {0};
-	int			nreplaces = 0;
+	HeapTuple	temp_ctup;
+	Form_pg_temp_class temp_pgcform;
+	bool		dirty;
+	bool		temp_dirty;
 	bool		result = true;
 
 	if (!statvalues->relpages.isnull)
@@ -171,52 +172,66 @@ relation_statistics_update_internal(Oid reloid,
 	 */
 	crel = table_open(RelationRelationId, RowExclusiveLock);
 
-	ctup = SearchSysCache1(RELOID, ObjectIdGetDatum(reloid));
+	ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(reloid));
 	if (!HeapTupleIsValid(ctup))
 		elog(ERROR, "pg_class entry for relid %u not found", reloid);
 
 	pgcform = (Form_pg_class) GETSTRUCT(ctup);
 
-	if (update_relpages && relpages != pgcform->relpages)
+	/*
+	 * For a global temporary table, need to update the pg_temp_class tuple
+	 * instead.  Force it into existence by opening the relation.
+	 */
+	if (pgcform->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
 	{
-		replaces[nreplaces] = Anum_pg_class_relpages;
-		values[nreplaces] = Int32GetDatum(relpages);
-		nreplaces++;
-	}
+		Relation	rel;
 
-	if (update_reltuples && reltuples != pgcform->reltuples)
-	{
-		replaces[nreplaces] = Anum_pg_class_reltuples;
-		values[nreplaces] = Float4GetDatum(reltuples);
-		nreplaces++;
-	}
+		rel = relation_open(reloid, AccessShareLock);
+		relation_close(rel, AccessShareLock);
 
-	if (update_relallvisible && relallvisible != pgcform->relallvisible)
-	{
-		replaces[nreplaces] = Anum_pg_class_relallvisible;
-		values[nreplaces] = Int32GetDatum(relallvisible);
-		nreplaces++;
-	}
+		temp_ctup = GetPgTempClassTuple(reloid);
+		if (!HeapTupleIsValid(temp_ctup))
+			elog(ERROR, "pg_temp_class entry for relid %u not found", reloid);
 
-	if (update_relallfrozen && relallfrozen != pgcform->relallfrozen)
+		temp_pgcform = (Form_pg_temp_class) GETSTRUCT(temp_ctup);
+	}
+	else
 	{
-		replaces[nreplaces] = Anum_pg_class_relallfrozen;
-		values[nreplaces] = Int32GetDatum(relallfrozen);
-		nreplaces++;
+		temp_ctup = NULL;
+		temp_pgcform = NULL;
 	}
 
-	if (nreplaces > 0)
+	dirty = false;
+	temp_dirty = false;
+
+	if (update_relpages)
+		SetEffective_relpages(pgcform, temp_pgcform, relpages,
+							  &dirty, &temp_dirty);
+
+	if (update_reltuples)
+		SetEffective_reltuples(pgcform, temp_pgcform, reltuples,
+							   &dirty, &temp_dirty);
+
+	if (update_relallvisible)
+		SetEffective_relallvisible(pgcform, temp_pgcform, relallvisible,
+								   &dirty, &temp_dirty);
+
+	if (update_relallfrozen)
+		SetEffective_relallfrozen(pgcform, temp_pgcform, relallfrozen,
+								  &dirty, &temp_dirty);
+
+	if (dirty)
+		CatalogTupleUpdate(crel, &ctup->t_self, ctup);
+
+	if (HeapTupleIsValid(temp_ctup))
 	{
-		TupleDesc	tupdesc = RelationGetDescr(crel);
-		HeapTuple	newtup;
+		if (temp_dirty)
+			UpdatePgTempClassTuple(reloid, temp_ctup);
 
-		newtup = heap_modify_tuple_by_cols(ctup, tupdesc, nreplaces,
-										   replaces, values, nulls);
-		CatalogTupleUpdate(crel, &newtup->t_self, newtup);
-		heap_freetuple(newtup);
+		heap_freetuple(temp_ctup);
 	}
 
-	ReleaseSysCache(ctup);
+	heap_freetuple(ctup);
 
 	/* release the lock, consistent with vac_update_relstats() */
 	table_close(crel, RowExclusiveLock);
diff --git a/src/backend/utils/cache/gtcatcache.c b/src/backend/utils/cache/gtcatcache.c
index a300aef4df9..82ca54c361d 100644
--- a/src/backend/utils/cache/gtcatcache.c
+++ b/src/backend/utils/cache/gtcatcache.c
@@ -208,7 +208,8 @@ initialize_cache(GTCatCache *cache)
  *	Find and update the cache entry tuple for the specified relation.
  */
 static GTCatCacheEntry *
-find_and_update_cache_entry(GTCatCache *cache, Oid relid, HeapTuple newtuple)
+find_and_update_cache_entry(GTCatCache *cache, Oid relid, HeapTuple newtuple,
+							bool update_in_place)
 {
 	SubTransactionId mySubid = GetCurrentSubTransactionId();
 	GTCatCacheEntry *entry;
@@ -225,10 +226,20 @@ find_and_update_cache_entry(GTCatCache *cache, Oid relid, HeapTuple newtuple)
 
 	Assert(HeapTupleIsValid(entry->tuple));
 
-	/* Update the cache entry, saving a copy for rollback, if necessary */
+	/*
+	 * Update the cache entry, saving a copy for rollback, if necessary.
+	 *
+	 * An in-place update of a database tuple is non-transactional (it isn't
+	 * affected by rollback), except if the tuple has already been updated in
+	 * the normal way in the same transaction, in which case rollback will
+	 * undo both the normal update and the in-place update.  Since we want to
+	 * keep our cache entry in sync with the database tuple, we must do an
+	 * in-place update of the cache entry in the same way, by updating the
+	 * most recent copy of the tuple, without saving a copy for rollback.
+	 */
 	oldcontext = MemoryContextSwitchTo(gt_cat_cache_tupctx);
 
-	if (entry->subid != mySubid)
+	if (entry->subid != mySubid && !update_in_place)
 	{
 		GTCatCacheEntry *save_entry;
 
@@ -685,7 +696,7 @@ GTCatCacheTupleUpdate(GTCatCacheIdentifier cacheId, Oid relid,
 	Relation	rel;
 
 	/* Find and update the cache entry for this relation */
-	entry = find_and_update_cache_entry(cache, relid, newtuple);
+	entry = find_and_update_cache_entry(cache, relid, newtuple, false);
 
 	/* Update the tuple in the database to match */
 	rel = table_open(cache->catalog_relid, RowExclusiveLock);
@@ -711,6 +722,59 @@ GTCatCacheTupleUpdate(GTCatCacheIdentifier cacheId, Oid relid,
 	table_close(rel, RowExclusiveLock);
 }
 
+/*
+ * GTCatCacheTupleUpdateInPlace
+ *
+ *	Do an in-place update of a catalog tuple for the specified relation.
+ *
+ *	Note: This updates both the cache entry, and the tuple in the database
+ *	(inserting it, if it hasn't already been written out).  This should not be
+ *	called while the database is read-only.
+ */
+void
+GTCatCacheTupleUpdateInPlace(GTCatCacheIdentifier cacheId, Oid relid,
+							 HeapTuple newtuple)
+{
+	GTCatCache *cache = &gt_cat_cache[cacheId];
+	GTCatCacheEntry *entry;
+	Relation	rel;
+
+	/* Find and update in place the cache entry for this relation */
+	entry = find_and_update_cache_entry(cache, relid, newtuple, true);
+
+	/* Do an in-place update of the tuple in the database */
+	rel = table_open(cache->catalog_relid, RowExclusiveLock);
+
+	if (entry->written)
+	{
+		ScanKeyData key[1];
+		HeapTuple	oldtuple;
+		void	   *inplace_state;
+
+		ScanKeyInit(&key[0],
+					cache->key_attno,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(relid));
+
+		systable_inplace_update_begin(rel, cache->index_relid, true, NULL,
+									  1, key, &oldtuple, &inplace_state);
+		if (!HeapTupleIsValid(oldtuple))
+			elog(ERROR, "cache lookup failed for global temp relation %u", relid);
+
+		ItemPointerCopy(&oldtuple->t_self, &newtuple->t_self);
+		systable_inplace_update_finish(inplace_state, newtuple);
+
+		heap_freetuple(oldtuple);
+	}
+	else
+	{
+		CatalogTupleInsert(rel, newtuple);
+		entry->written = true;
+	}
+
+	table_close(rel, RowExclusiveLock);
+}
+
 /*
  * GTCatCacheTupleDelete
  *
@@ -730,7 +794,7 @@ GTCatCacheTupleDelete(GTCatCacheIdentifier cacheId, Oid relid)
 	 * Find and update the cache entry for this relation, setting its tuple to
 	 * NULL, and marking it as deleted.
 	 */
-	entry = find_and_update_cache_entry(cache, relid, NULL);
+	entry = find_and_update_cache_entry(cache, relid, NULL, false);
 	entry->deleted = true;
 
 	/*
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index 80675888b89..1156f5f5eaa 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -2419,6 +2419,17 @@ get_rel_persistence(Oid relid)
 	return result;
 }
 
+/*
+ * rel_is_global_temp
+ *
+ *		Returns true if the given relation is a global temporary relation.
+ */
+bool
+rel_is_global_temp(Oid relid)
+{
+	return get_rel_persistence(relid) == RELPERSISTENCE_GLOBAL_TEMP;
+}
+
 /*
  * get_rel_relam
  *
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index cef2435c195..868492d8f00 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4038,10 +4038,11 @@ RelationSetNewRelfilenumber(Relation relation, char persistence)
 		/* relpages etc. never change for sequences */
 		if (relation->rd_rel->relkind != RELKIND_SEQUENCE)
 		{
-			classform->relpages = 0;	/* it's empty until further notice */
-			classform->reltuples = -1;
-			classform->relallvisible = 0;
-			classform->relallfrozen = 0;
+			/* it's empty until further notice */
+			SetEffective_relpages(classform, temp_classform, 0, NULL, NULL);
+			SetEffective_reltuples(classform, temp_classform, -1, NULL, NULL);
+			SetEffective_relallvisible(classform, temp_classform, 0, NULL, NULL);
+			SetEffective_relallfrozen(classform, temp_classform, 0, NULL, NULL);
 		}
 		classform->relfrozenxid = freezeXid;
 		classform->relminmxid = minmulti;
diff --git a/src/include/catalog/pg_temp_class.h b/src/include/catalog/pg_temp_class.h
index 7c1e711a5f6..55ba499ae5d 100644
--- a/src/include/catalog/pg_temp_class.h
+++ b/src/include/catalog/pg_temp_class.h
@@ -45,6 +45,18 @@ CATALOG(pg_temp_class,8082,TempRelationRelationId) BKI_TEMP_RELATION
 
 	/* identifier of table space for relation (0 means default for database) */
 	Oid			reltablespace BKI_DEFAULT(0) BKI_LOOKUP_OPT(pg_tablespace);
+
+	/* # of blocks (not always up-to-date) */
+	int32		relpages BKI_DEFAULT(0);
+
+	/* # of tuples (not always up-to-date; -1 means "unknown") */
+	float4		reltuples BKI_DEFAULT(-1);
+
+	/* # of all-visible blocks (not always up-to-date) */
+	int32		relallvisible BKI_DEFAULT(0);
+
+	/* # of all-frozen blocks (not always up-to-date) */
+	int32		relallfrozen BKI_DEFAULT(0);
 } FormData_pg_temp_class;
 
 END_CATALOG_STRUCT
@@ -71,6 +83,10 @@ MAKE_SYSCACHE(TEMPRELOID, pg_temp_class_oid_index, 128);
 		(target)->oid = (source)->oid; \
 		(target)->relfilenode = (source)->relfilenode; \
 		(target)->reltablespace = (source)->reltablespace; \
+		(target)->relpages = (source)->relpages; \
+		(target)->reltuples = (source)->reltuples; \
+		(target)->relallvisible = (source)->relallvisible; \
+		(target)->relallfrozen = (source)->relallfrozen; \
 	} while (0)
 
 /*
@@ -93,6 +109,46 @@ GetEffective_reltablespace(Form_pg_class cf, Form_pg_temp_class tf)
 	return tf != NULL ? tf->reltablespace : cf->reltablespace;
 }
 
+/*
+ * Get the effective value of relpages from pg_class and pg_temp_class tuple
+ * data.  The value from pg_temp_class (if present) takes precedence.
+ */
+static inline int32
+GetEffective_relpages(Form_pg_class cf, Form_pg_temp_class tf)
+{
+	return tf != NULL ? tf->relpages : cf->relpages;
+}
+
+/*
+ * Get the effective value of reltuples from pg_class and pg_temp_class tuple
+ * data.  The value from pg_temp_class (if present) takes precedence.
+ */
+static inline float4
+GetEffective_reltuples(Form_pg_class cf, Form_pg_temp_class tf)
+{
+	return tf != NULL ? tf->reltuples : cf->reltuples;
+}
+
+/*
+ * Get the effective value of relallvisible from pg_class and pg_temp_class
+ * tuple data.  The value from pg_temp_class (if present) takes precedence.
+ */
+static inline int32
+GetEffective_relallvisible(Form_pg_class cf, Form_pg_temp_class tf)
+{
+	return tf != NULL ? tf->relallvisible : cf->relallvisible;
+}
+
+/*
+ * Get the effective value of relallfrozen from pg_class and pg_temp_class
+ * tuple data.  The value from pg_temp_class (if present) takes precedence.
+ */
+static inline int32
+GetEffective_relallfrozen(Form_pg_class cf, Form_pg_temp_class tf)
+{
+	return tf != NULL ? tf->relallfrozen : cf->relallfrozen;
+}
+
 /*
  * Set the effective value of relfilenode in tuple form data from pg_class or
  * pg_temp_class.  The value is set in pg_temp_class instead of pg_class, if
@@ -121,10 +177,119 @@ SetEffective_reltablespace(Form_pg_class cf, Form_pg_temp_class tf, Oid val)
 		tf->reltablespace = val;
 }
 
+/*
+ * Set the effective value of relpages in tuple form data from pg_class or
+ * pg_temp_class.  The value is set in pg_temp_class instead of pg_class, if
+ * the pg_temp_class tuple form data is non-NULL.  If non-NULL, the cdirty or
+ * tdirty flag is updated, if the value actually changes.
+ */
+static inline void
+SetEffective_relpages(Form_pg_class cf, Form_pg_temp_class tf, int32 val,
+					  bool *cdirty, bool *tdirty)
+{
+	if (tf != NULL)
+	{
+		if (val != tf->relpages)
+		{
+			tf->relpages = val;
+			if (tdirty != NULL)
+				*tdirty = true;
+		}
+	}
+	else if (val != cf->relpages)
+	{
+		cf->relpages = val;
+		if (cdirty != NULL)
+			*cdirty = true;
+	}
+}
+
+/*
+ * Set the effective value of reltuples in tuple form data from pg_class or
+ * pg_temp_class.  The value is set in pg_temp_class instead of pg_class, if
+ * the pg_temp_class tuple form data is non-NULL.  If non-NULL, the cdirty or
+ * tdirty flag is updated, if the value actually changes.
+ */
+static inline void
+SetEffective_reltuples(Form_pg_class cf, Form_pg_temp_class tf, float4 val,
+					   bool *cdirty, bool *tdirty)
+{
+	if (tf != NULL)
+	{
+		if (val != tf->reltuples)
+		{
+			tf->reltuples = val;
+			if (tdirty != NULL)
+				*tdirty = true;
+		}
+	}
+	else if (val != cf->reltuples)
+	{
+		cf->reltuples = val;
+		if (cdirty != NULL)
+			*cdirty = true;
+	}
+}
+
+/*
+ * Set the effective value of relallvisible in tuple form data from pg_class
+ * or pg_temp_class.  The value is set in pg_temp_class instead of pg_class,
+ * if the pg_temp_class tuple form data is non-NULL.  If non-NULL, the cdirty
+ * or tdirty flag is updated, if the value actually changes.
+ */
+static inline void
+SetEffective_relallvisible(Form_pg_class cf, Form_pg_temp_class tf, int32 val,
+						   bool *cdirty, bool *tdirty)
+{
+	if (tf != NULL)
+	{
+		if (val != tf->relallvisible)
+		{
+			tf->relallvisible = val;
+			if (tdirty != NULL)
+				*tdirty = true;
+		}
+	}
+	else if (val != cf->relallvisible)
+	{
+		cf->relallvisible = val;
+		if (cdirty != NULL)
+			*cdirty = true;
+	}
+}
+
+/*
+ * Set the effective value of relallfrozen in tuple form data from pg_class or
+ * pg_temp_class.  The value is set in pg_temp_class instead of pg_class, if
+ * the pg_temp_class tuple form data is non-NULL.  If non-NULL, the cdirty or
+ * tdirty flag is updated, if the value actually changes.
+ */
+static inline void
+SetEffective_relallfrozen(Form_pg_class cf, Form_pg_temp_class tf, int32 val,
+						  bool *cdirty, bool *tdirty)
+{
+	if (tf != NULL)
+	{
+		if (val != tf->relallfrozen)
+		{
+			tf->relallfrozen = val;
+			if (tdirty != NULL)
+				*tdirty = true;
+		}
+	}
+	else if (val != cf->relallfrozen)
+	{
+		cf->relallfrozen = val;
+		if (cdirty != NULL)
+			*cdirty = true;
+	}
+}
+
 extern bool PgTempClassTupleExists(Oid relid);
 extern HeapTuple GetPgTempClassTuple(Oid relid);
 extern void InsertPgTempClassTuple(Relation rel);
 extern void UpdatePgTempClassTuple(Oid relid, HeapTuple newtuple);
+extern void UpdatePgTempClassTupleInPlace(Oid relid, HeapTuple newtuple);
 extern void DeletePgTempClassTuple(Oid relid);
 extern HeapTuple GetPgClassAndPgTempClassTuples(Oid relid, bool lock_tuple,
 												HeapTuple *temp_tuple,
diff --git a/src/include/utils/gtcatcache.h b/src/include/utils/gtcatcache.h
index 99a115ab99b..ab57d4c5d4d 100644
--- a/src/include/utils/gtcatcache.h
+++ b/src/include/utils/gtcatcache.h
@@ -32,6 +32,8 @@ extern void GTCatCacheTupleInsert(GTCatCacheIdentifier cacheId, Oid relid,
 								  const Datum *values, const bool *nulls);
 extern void GTCatCacheTupleUpdate(GTCatCacheIdentifier cacheId, Oid relid,
 								  HeapTuple newtuple);
+extern void GTCatCacheTupleUpdateInPlace(GTCatCacheIdentifier cacheId,
+										 Oid relid, HeapTuple newtuple);
 extern void GTCatCacheTupleDelete(GTCatCacheIdentifier cacheId, Oid relid);
 extern void GTCatCacheFlush(void);
 extern void AtEOXact_GTCatCache(bool isCommit);
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 865980cb0f1..c556b667c5e 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -150,6 +150,7 @@ extern char get_rel_relkind(Oid relid);
 extern bool get_rel_relispartition(Oid relid);
 extern Oid	get_rel_tablespace(Oid relid);
 extern char get_rel_persistence(Oid relid);
+extern bool rel_is_global_temp(Oid relid);
 extern Oid	get_rel_relam(Oid relid);
 extern Oid	get_transform_fromsql(Oid typid, Oid langid, List *trftypes);
 extern Oid	get_transform_tosql(Oid typid, Oid langid, List *trftypes);
diff --git a/src/test/isolation/expected/global-temp.out b/src/test/isolation/expected/global-temp.out
index 0c5bc5887e5..a4da2d63ca3 100644
--- a/src/test/isolation/expected/global-temp.out
+++ b/src/test/isolation/expected/global-temp.out
@@ -271,6 +271,38 @@ key|pg_typeof|val
 
 step drop1: DROP TABLE tmp2;
 
+starting permutation: create1dr b1 b2 ins1_2 ins2_2 sel1_2 sel2_2 c1 c2 sel1_2 sel2_2 drop1
+step create1dr: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text) ON COMMIT DELETE ROWS;
+step b1: BEGIN;
+step b2: BEGIN;
+step ins1_2: INSERT INTO tmp2 VALUES (1, 's1');
+step ins2_2: INSERT INTO tmp2 VALUES (1, 's2');
+step sel1_2: SELECT * FROM tmp2;
+key|val
+---+---
+  1|s1 
+(1 row)
+
+step sel2_2: SELECT * FROM tmp2;
+key|val
+---+---
+  1|s2 
+(1 row)
+
+step c1: COMMIT;
+step c2: COMMIT;
+step sel1_2: SELECT * FROM tmp2;
+key|val
+---+---
+(0 rows)
+
+step sel2_2: SELECT * FROM tmp2;
+key|val
+---+---
+(0 rows)
+
+step drop1: DROP TABLE tmp2;
+
 starting permutation: create1dr ins1_2 ins2_2 drop1 create1dr ins1_2 ins2_2 drop1
 step create1dr: CREATE GLOBAL TEMP TABLE tmp2 (key int, val text) ON COMMIT DELETE ROWS;
 step ins1_2: INSERT INTO tmp2 VALUES (1, 's1');
diff --git a/src/test/isolation/specs/global-temp.spec b/src/test/isolation/specs/global-temp.spec
index d4457f6c787..2df803c00fb 100644
--- a/src/test/isolation/specs/global-temp.spec
+++ b/src/test/isolation/specs/global-temp.spec
@@ -26,6 +26,7 @@ step sel1p { SELECT tableoid::regclass, * FROM tmp_parted; }
 step create1 { CREATE GLOBAL TEMP TABLE tmp2 (key int, val text, icol int, bcol box); }
 step create1dr { CREATE GLOBAL TEMP TABLE tmp2 (key int, val text) ON COMMIT DELETE ROWS; }
 step ins1_2 { INSERT INTO tmp2 VALUES (1, 's1'); }
+step sel1_2 { SELECT * FROM tmp2; }
 step alter1a { ALTER TABLE tmp2 ALTER COLUMN key SET DATA TYPE numeric; }
 step alter1b { ALTER TABLE tmp2 ALTER COLUMN val SET NOT NULL; }
 step alter1c { ALTER TABLE tmp2 ADD CONSTRAINT tmp2_nn NOT NULL key; }
@@ -41,6 +42,7 @@ step r1 { ROLLBACK; }
 step sp1 { SAVEPOINT sp; }
 step rsp1 { ROLLBACK TO SAVEPOINT sp; }
 step drop1 { DROP TABLE tmp2; }
+step c1 { COMMIT; }
 step prep1 { PREPARE TRANSACTION 'tx'; }
 step cprep1 { COMMIT PREPARED 'tx'; }
 step idx1 { CREATE INDEX tmp_val_idx ON tmp(val); }
@@ -77,6 +79,7 @@ step r2 { ROLLBACK; }
 step sp2 { SAVEPOINT sp; }
 step rsp2 { ROLLBACK TO SAVEPOINT sp; }
 step ins2_2 { INSERT INTO tmp2 VALUES (1, 's2'); }
+step sel2_2 { SELECT * FROM tmp2; }
 step seltype2 { SELECT key, pg_typeof(key), val FROM tmp2; }
 step drop2 { DROP TABLE tmp2; }
 step sel2_idx {
@@ -119,6 +122,9 @@ permutation create1 ins1_2 ins2_2
             alter1a alter1b alter1c alter1d alter1e alter1f alter1g alter1h
             uniq_idx1 seltype1 seltype2 drop1
 
+# Test concurrent ON COMMIT DELETE ROWS
+permutation create1dr b1 b2 ins1_2 ins2_2 sel1_2 sel2_2 c1 c2 sel1_2 sel2_2 drop1
+
 # Test DROP with ON COMMIT DELETE ROWS
 permutation create1dr ins1_2 ins2_2 drop1 create1dr ins1_2 ins2_2 drop1
 
diff --git a/src/test/regress/expected/global_temp.out b/src/test/regress/expected/global_temp.out
index a2858479fc7..178cc681448 100644
--- a/src/test/regress/expected/global_temp.out
+++ b/src/test/regress/expected/global_temp.out
@@ -517,6 +517,214 @@ SELECT * FROM tmp1;
  1 | xxx | 1
 (1 row)
 
+-- Test stats updates applied by CREATE INDEX, ANALYZE, VACUUM, and REPACK
+CREATE GLOBAL TEMP TABLE tmp2 (a int);
+INSERT INTO tmp2 SELECT * FROM generate_series(1, 100);
+SELECT c.oid::regclass,
+       c.relpages AS global_relpages, c.reltuples AS global_reltuples,
+       CASE WHEN t.relpages = 0 THEN 'zero' ELSE 'non-zero' END AS local_relpages,
+       t.reltuples AS local_reltuples
+  FROM pg_class c JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.oid = 'tmp2'::regclass;
+ oid  | global_relpages | global_reltuples | local_relpages | local_reltuples 
+------+-----------------+------------------+----------------+-----------------
+ tmp2 |               0 |               -1 | zero           |              -1
+(1 row)
+
+CREATE INDEX tmp2_a_idx ON tmp2(a);
+SELECT c.oid::regclass,
+       c.relpages AS global_relpages, c.reltuples AS global_reltuples,
+       CASE WHEN t.relpages = 0 THEN 'zero' ELSE 'non-zero' END AS local_relpages,
+       t.reltuples AS local_reltuples
+  FROM pg_class c JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.oid = 'tmp2'::regclass OR c.oid = 'tmp2_a_idx'::regclass ORDER BY 1;
+    oid     | global_relpages | global_reltuples | local_relpages | local_reltuples 
+------------+-----------------+------------------+----------------+-----------------
+ tmp2       |               0 |               -1 | non-zero       |             100
+ tmp2_a_idx |               0 |                0 | non-zero       |             100
+(2 rows)
+
+INSERT INTO tmp2 SELECT * FROM generate_series(101, 300);
+ANALYZE tmp2;
+SELECT c.oid::regclass,
+       c.relpages AS global_relpages, c.reltuples AS global_reltuples,
+       CASE WHEN t.relpages = 0 THEN 'zero' ELSE 'non-zero' END AS local_relpages,
+       t.reltuples AS local_reltuples
+  FROM pg_class c JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.oid = 'tmp2'::regclass OR c.oid = 'tmp2_a_idx'::regclass ORDER BY 1;
+    oid     | global_relpages | global_reltuples | local_relpages | local_reltuples 
+------------+-----------------+------------------+----------------+-----------------
+ tmp2       |               0 |               -1 | non-zero       |             300
+ tmp2_a_idx |               0 |                0 | non-zero       |             300
+(2 rows)
+
+DELETE FROM tmp2 WHERE a % 2 = 0;
+VACUUM ANALYZE tmp2;
+SELECT c.oid::regclass,
+       c.relpages AS global_relpages, c.reltuples AS global_reltuples,
+       CASE WHEN t.relpages = 0 THEN 'zero' ELSE 'non-zero' END AS local_relpages,
+       t.reltuples AS local_reltuples
+  FROM pg_class c JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.oid = 'tmp2'::regclass OR c.oid = 'tmp2_a_idx'::regclass ORDER BY 1;
+    oid     | global_relpages | global_reltuples | local_relpages | local_reltuples 
+------------+-----------------+------------------+----------------+-----------------
+ tmp2       |               0 |               -1 | non-zero       |             150
+ tmp2_a_idx |               0 |                0 | non-zero       |             150
+(2 rows)
+
+DELETE FROM tmp2 WHERE a % 3 = 0;
+REPACK (ANALYZE) tmp2;
+SELECT c.oid::regclass,
+       c.relpages AS global_relpages, c.reltuples AS global_reltuples,
+       CASE WHEN t.relpages = 0 THEN 'zero' ELSE 'non-zero' END AS local_relpages,
+       t.reltuples AS local_reltuples
+  FROM pg_class c JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.oid = 'tmp2'::regclass OR c.oid = 'tmp2_a_idx'::regclass ORDER BY 1;
+    oid     | global_relpages | global_reltuples | local_relpages | local_reltuples 
+------------+-----------------+------------------+----------------+-----------------
+ tmp2       |               0 |               -1 | non-zero       |             100
+ tmp2_a_idx |               0 |                0 | non-zero       |             100
+(2 rows)
+
+-- Test stats usage
+CREATE FUNCTION row_estimate(query text) RETURNS int
+LANGUAGE plpgsql AS
+$$
+DECLARE
+  line text;
+BEGIN
+  FOR line IN EXECUTE FORMAT('EXPLAIN %s', query)
+  LOOP
+    RETURN (regexp_match(line, 'rows=(\d*)'))[1]::int;
+  END LOOP;
+END;
+$$;
+SELECT row_estimate('SELECT * FROM tmp2');
+ row_estimate 
+--------------
+          100
+(1 row)
+
+-- Test in-place stats update (non-transactional)
+TRUNCATE tmp2;
+SELECT reltuples FROM pg_temp_class WHERE oid = 'tmp2'::regclass;
+ reltuples 
+-----------
+        -1
+(1 row)
+
+SELECT row_estimate('SELECT * FROM tmp2');
+ row_estimate 
+--------------
+         2550
+(1 row)
+
+BEGIN;
+INSERT INTO tmp2 SELECT * FROM generate_series(1, 100);
+ANALYZE tmp2;
+SELECT reltuples FROM pg_temp_class WHERE oid = 'tmp2'::regclass;
+ reltuples 
+-----------
+       100
+(1 row)
+
+SELECT row_estimate('SELECT * FROM tmp2');
+ row_estimate 
+--------------
+          100
+(1 row)
+
+ROLLBACK;
+SELECT reltuples FROM pg_temp_class WHERE oid = 'tmp2'::regclass;
+ reltuples 
+-----------
+       100
+(1 row)
+
+SELECT row_estimate('SELECT * FROM tmp2');
+ row_estimate 
+--------------
+          100
+(1 row)
+
+-- Test in-place stats update after regular update (transactional)
+BEGIN;
+TRUNCATE tmp2;
+INSERT INTO tmp2 SELECT * FROM generate_series(1, 50);
+ANALYZE tmp2;
+SELECT reltuples FROM pg_temp_class WHERE oid = 'tmp2'::regclass;
+ reltuples 
+-----------
+        50
+(1 row)
+
+SELECT row_estimate('SELECT * FROM tmp2');
+ row_estimate 
+--------------
+           50
+(1 row)
+
+ROLLBACK;
+SELECT reltuples FROM pg_temp_class WHERE oid = 'tmp2'::regclass;
+ reltuples 
+-----------
+       100
+(1 row)
+
+SELECT row_estimate('SELECT * FROM tmp2');
+ row_estimate 
+--------------
+          100
+(1 row)
+
+-- Test manually updating stats
+SELECT pg_clear_relation_stats('global_temp_tests', 'tmp2');
+ pg_clear_relation_stats 
+-------------------------
+ 
+(1 row)
+
+SELECT oid::regclass, relpages, reltuples, relallvisible, relallfrozen
+  FROM pg_class WHERE oid = 'tmp2'::regclass;
+ oid  | relpages | reltuples | relallvisible | relallfrozen 
+------+----------+-----------+---------------+--------------
+ tmp2 |        0 |        -1 |             0 |            0
+(1 row)
+
+SELECT oid::regclass, relpages, reltuples, relallvisible, relallfrozen
+  FROM pg_temp_class WHERE oid = 'tmp2'::regclass;
+ oid  | relpages | reltuples | relallvisible | relallfrozen 
+------+----------+-----------+---------------+--------------
+ tmp2 |        0 |        -1 |             0 |            0
+(1 row)
+
+SELECT pg_restore_relation_stats(
+  'schemaname', 'global_temp_tests',
+  'relname', 'tmp2',
+  'relpages', 5,
+  'reltuples', 150::real,
+  'relallvisible', 10,
+  'relallfrozen', 20);
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
+SELECT oid::regclass, relpages, reltuples, relallvisible, relallfrozen
+  FROM pg_class WHERE oid = 'tmp2'::regclass;
+ oid  | relpages | reltuples | relallvisible | relallfrozen 
+------+----------+-----------+---------------+--------------
+ tmp2 |        0 |        -1 |             0 |            0
+(1 row)
+
+SELECT oid::regclass, relpages, reltuples, relallvisible, relallfrozen
+  FROM pg_temp_class WHERE oid = 'tmp2'::regclass;
+ oid  | relpages | reltuples | relallvisible | relallfrozen 
+------+----------+-----------+---------------+--------------
+ tmp2 |        5 |       150 |            10 |           20
+(1 row)
+
+DROP TABLE tmp2;
 -- Test view creation
 CREATE VIEW v AS SELECT * FROM tmp1;
 SELECT * FROM v;
diff --git a/src/test/regress/sql/global_temp.sql b/src/test/regress/sql/global_temp.sql
index 9b24eccf028..ef9902e5212 100644
--- a/src/test/regress/sql/global_temp.sql
+++ b/src/test/regress/sql/global_temp.sql
@@ -295,6 +295,115 @@ COMMIT;
 SELECT oid::regclass FROM pg_temp_class ORDER BY 1;
 SELECT * FROM tmp1;
 
+-- Test stats updates applied by CREATE INDEX, ANALYZE, VACUUM, and REPACK
+CREATE GLOBAL TEMP TABLE tmp2 (a int);
+INSERT INTO tmp2 SELECT * FROM generate_series(1, 100);
+SELECT c.oid::regclass,
+       c.relpages AS global_relpages, c.reltuples AS global_reltuples,
+       CASE WHEN t.relpages = 0 THEN 'zero' ELSE 'non-zero' END AS local_relpages,
+       t.reltuples AS local_reltuples
+  FROM pg_class c JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.oid = 'tmp2'::regclass;
+
+CREATE INDEX tmp2_a_idx ON tmp2(a);
+SELECT c.oid::regclass,
+       c.relpages AS global_relpages, c.reltuples AS global_reltuples,
+       CASE WHEN t.relpages = 0 THEN 'zero' ELSE 'non-zero' END AS local_relpages,
+       t.reltuples AS local_reltuples
+  FROM pg_class c JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.oid = 'tmp2'::regclass OR c.oid = 'tmp2_a_idx'::regclass ORDER BY 1;
+
+INSERT INTO tmp2 SELECT * FROM generate_series(101, 300);
+ANALYZE tmp2;
+SELECT c.oid::regclass,
+       c.relpages AS global_relpages, c.reltuples AS global_reltuples,
+       CASE WHEN t.relpages = 0 THEN 'zero' ELSE 'non-zero' END AS local_relpages,
+       t.reltuples AS local_reltuples
+  FROM pg_class c JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.oid = 'tmp2'::regclass OR c.oid = 'tmp2_a_idx'::regclass ORDER BY 1;
+
+DELETE FROM tmp2 WHERE a % 2 = 0;
+VACUUM ANALYZE tmp2;
+SELECT c.oid::regclass,
+       c.relpages AS global_relpages, c.reltuples AS global_reltuples,
+       CASE WHEN t.relpages = 0 THEN 'zero' ELSE 'non-zero' END AS local_relpages,
+       t.reltuples AS local_reltuples
+  FROM pg_class c JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.oid = 'tmp2'::regclass OR c.oid = 'tmp2_a_idx'::regclass ORDER BY 1;
+
+DELETE FROM tmp2 WHERE a % 3 = 0;
+REPACK (ANALYZE) tmp2;
+SELECT c.oid::regclass,
+       c.relpages AS global_relpages, c.reltuples AS global_reltuples,
+       CASE WHEN t.relpages = 0 THEN 'zero' ELSE 'non-zero' END AS local_relpages,
+       t.reltuples AS local_reltuples
+  FROM pg_class c JOIN pg_temp_class t ON t.oid = c.oid
+ WHERE c.oid = 'tmp2'::regclass OR c.oid = 'tmp2_a_idx'::regclass ORDER BY 1;
+
+-- Test stats usage
+CREATE FUNCTION row_estimate(query text) RETURNS int
+LANGUAGE plpgsql AS
+$$
+DECLARE
+  line text;
+BEGIN
+  FOR line IN EXECUTE FORMAT('EXPLAIN %s', query)
+  LOOP
+    RETURN (regexp_match(line, 'rows=(\d*)'))[1]::int;
+  END LOOP;
+END;
+$$;
+
+SELECT row_estimate('SELECT * FROM tmp2');
+
+-- Test in-place stats update (non-transactional)
+TRUNCATE tmp2;
+SELECT reltuples FROM pg_temp_class WHERE oid = 'tmp2'::regclass;
+SELECT row_estimate('SELECT * FROM tmp2');
+
+BEGIN;
+INSERT INTO tmp2 SELECT * FROM generate_series(1, 100);
+ANALYZE tmp2;
+SELECT reltuples FROM pg_temp_class WHERE oid = 'tmp2'::regclass;
+SELECT row_estimate('SELECT * FROM tmp2');
+ROLLBACK;
+
+SELECT reltuples FROM pg_temp_class WHERE oid = 'tmp2'::regclass;
+SELECT row_estimate('SELECT * FROM tmp2');
+
+-- Test in-place stats update after regular update (transactional)
+BEGIN;
+TRUNCATE tmp2;
+INSERT INTO tmp2 SELECT * FROM generate_series(1, 50);
+ANALYZE tmp2;
+SELECT reltuples FROM pg_temp_class WHERE oid = 'tmp2'::regclass;
+SELECT row_estimate('SELECT * FROM tmp2');
+ROLLBACK;
+
+SELECT reltuples FROM pg_temp_class WHERE oid = 'tmp2'::regclass;
+SELECT row_estimate('SELECT * FROM tmp2');
+
+-- Test manually updating stats
+SELECT pg_clear_relation_stats('global_temp_tests', 'tmp2');
+SELECT oid::regclass, relpages, reltuples, relallvisible, relallfrozen
+  FROM pg_class WHERE oid = 'tmp2'::regclass;
+SELECT oid::regclass, relpages, reltuples, relallvisible, relallfrozen
+  FROM pg_temp_class WHERE oid = 'tmp2'::regclass;
+
+SELECT pg_restore_relation_stats(
+  'schemaname', 'global_temp_tests',
+  'relname', 'tmp2',
+  'relpages', 5,
+  'reltuples', 150::real,
+  'relallvisible', 10,
+  'relallfrozen', 20);
+SELECT oid::regclass, relpages, reltuples, relallvisible, relallfrozen
+  FROM pg_class WHERE oid = 'tmp2'::regclass;
+SELECT oid::regclass, relpages, reltuples, relallvisible, relallfrozen
+  FROM pg_temp_class WHERE oid = 'tmp2'::regclass;
+
+DROP TABLE tmp2;
+
 -- Test view creation
 CREATE VIEW v AS SELECT * FROM tmp1;
 SELECT * FROM v;
-- 
2.43.0

