From 8dc26f24a26c478b15e7233fee74d4f367d517f1 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 v11 5/9] Support relation statistics on global temporary
 relations.

This adds relpages, reltuples, relallvisible, and relallfrozen to the
in-memory information held for global temporary relations, allowing
them to operate independently in each session. ANALYZE, CREATE INDEX,
REPACK, VACUUM, and pg_clear/restore_relation_stats() are updated to
keep these statistics in memory, instead of updating 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                  |  26 ++-
 doc/src/sgml/func/func-admin.sgml           |   5 +-
 doc/src/sgml/func/func-info.sgml            |  12 +-
 doc/src/sgml/indexam.sgml                   |   3 +-
 doc/src/sgml/maintenance.sgml               |  13 +-
 doc/src/sgml/monitoring.sgml                |  21 +-
 doc/src/sgml/perform.sgml                   |   8 +-
 doc/src/sgml/planstats.sgml                 |  18 +-
 src/backend/catalog/global_temp.c           |  40 +++-
 src/backend/catalog/heap.c                  |  41 +++-
 src/backend/catalog/index.c                 | 100 ++++----
 src/backend/commands/repack.c               |  56 +++--
 src/backend/commands/vacuum.c               |  60 +++--
 src/backend/statistics/relation_stats.c     |  67 +++---
 src/backend/utils/cache/relcache.c          |   9 +-
 src/include/catalog/global_temp.h           | 169 ++++++++++++++
 src/include/catalog/pg_proc.dat             |  12 +-
 src/test/isolation/expected/global-temp.out |  32 +++
 src/test/isolation/specs/global-temp.spec   |  10 +-
 src/test/regress/expected/global_temp.out   | 246 ++++++++++++++++++--
 src/test/regress/sql/global_temp.sql        | 123 +++++++++-
 21 files changed, 897 insertions(+), 174 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 6f102a84d15..f06be5883f6 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2048,6 +2048,12 @@ 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
+       <structfield>relpages</structfield> value returned by
+       <link linkend="pg-gtr-info"><function>pg_gtr_info()</function></link>,
+       if non-null, takes precedence over the value from this catalog.
       </para></entry>
      </row>
 
@@ -2064,6 +2070,12 @@ 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
+       <structfield>reltuples</structfield> value returned by
+       <link linkend="pg-gtr-info"><function>pg_gtr_info()</function></link>,
+       if non-null, takes precedence over the value from this catalog.
       </para></entry>
      </row>
 
@@ -2081,6 +2093,12 @@ 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
+       <structfield>relallvisible</structfield> value returned by
+       <link linkend="pg-gtr-info"><function>pg_gtr_info()</function></link>,
+       if non-null, takes precedence over the value from this catalog.
       </para></entry>
      </row>
 
@@ -2095,7 +2113,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>,
@@ -2105,10 +2122,15 @@ 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
+       <structfield>relallfrozen</structfield> value returned by
+       <link linkend="pg-gtr-info"><function>pg_gtr_info()</function></link>,
+       if non-null, 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>
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 7f761946357..8dd9fe06c65 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -2037,7 +2037,10 @@ SELECT pg_restore_relation_stats(
          <type>integer</type>, <literal>reltuples</literal> with a value of
          type <type>real</type>, <literal>relallvisible</literal> with a value
          of type <type>integer</type>, and <literal>relallfrozen</literal>
-         with a value of type <type>integer</type>.
+         with a value of type <type>integer</type>.  For a global temporary
+         table, the values are not stored in <structname>pg_class</structname>,
+         but are instead held in memory, and may be retrieved using
+         <link linkend="pg-gtr-info"><function>pg_gtr_info()</function></link>.
         </para>
         <para>
          Additionally, this function accepts argument name
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index 9e30b03ca23..bfc2e4d7811 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -372,7 +372,11 @@
         <function>pg_gtr_info</function> ( <type>oid</type> )
         <returnvalue>record</returnvalue>
         ( <parameter>relfilenode</parameter> <type>oid</type>,
-        <parameter>reltablespace</parameter> <type>oid</type> )
+        <parameter>reltablespace</parameter> <type>oid</type>,
+        <parameter>relpages</parameter> <type>int4</type>,
+        <parameter>reltuples</parameter> <type>float4</type>,
+        <parameter>relallvisible</parameter> <type>int4</type>,
+        <parameter>relallfrozen</parameter> <type>int4</type> )
        </para>
        <para>
         Returns information about a global temporary relation being used in
@@ -397,7 +401,11 @@
         <returnvalue>setof record</returnvalue>
         ( <parameter>oid</parameter> <type>oid</type>,
         <parameter>relfilenode</parameter> <type>oid</type>,
-        <parameter>reltablespace</parameter> <type>oid</type> )
+        <parameter>reltablespace</parameter> <type>oid</type>,
+        <parameter>relpages</parameter> <type>int4</type>,
+        <parameter>reltuples</parameter> <type>float4</type>,
+        <parameter>relallvisible</parameter> <type>int4</type>,
+        <parameter>relallfrozen</parameter> <type>int4</type> )
        </para>
        <para>
         Returns information about all global temporary relations being used in
diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml
index 6649bdd6cd1..81315b3fa46 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 held in memory 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 137175ca3b5..7ceb7c78a10 100644
--- a/doc/src/sgml/maintenance.sgml
+++ b/doc/src/sgml/maintenance.sgml
@@ -973,7 +973,10 @@ 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 the <structfield>reltuples</structfield> value returned by
+    <link linkend="pg-gtr-info"><function>pg_gtr_info()</function></link>,
+    for a global temporary table.
    </para>
 
    <para>
@@ -988,8 +991,14 @@ vacuum insert threshold = vacuum base insert threshold + vacuum insert scale fac
     <xref linkend="guc-autovacuum-vacuum-insert-scale-factor"/>,
     the number of tuples is
     <structname>pg_class</structname>.<structfield>reltuples</structfield>,
+    or the <structfield>reltuples</structfield> value returned by
+    <link linkend="pg-gtr-info"><function>pg_gtr_info()</function></link>,
+    for a global temporary table,
     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 the equivalent formula based on the values returned by
+    <link linkend="pg-gtr-info"><function>pg_gtr_info()</function></link>,
+    for a global temporary table.
     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 62dadf3e86c..f222cb54a30 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -6709,7 +6709,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 the
+       in-memory statistics for a global temporary table. When this
        phase is completed, <command>ANALYZE</command> will end.
       </entry>
      </row>
@@ -8005,7 +8006,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 in memory, for a global temporary table),
+       and report statistics to the cumulative
        statistics system. When this phase is completed, <command>VACUUM</command> will end.
      </entry>
     </row>
@@ -9206,6 +9208,21 @@ 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 returned by
+    <link linkend="pg-gtr-info"><function>pg_gtr_info()</function></link>,
+    if it's non-null, instead of the values from
+    <structname>pg_class</structname>:
+<programlisting>
+SELECT pg_relation_filepath(oid), COALESCE(t.relpages, c.relpages) AS relpages
+FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+WHERE c.relname = 'customer';
+
+ 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..e21635b385c 100644
--- a/doc/src/sgml/perform.sgml
+++ b/doc/src/sgml/perform.sgml
@@ -1283,8 +1283,10 @@ EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 &lt; 100 AND unique2 &gt; 9000
    by each table and index.  This information is kept in the table
    <link linkend="catalog-pg-class"><structname>pg_class</structname></link>,
    in the columns <structfield>reltuples</structfield> and
-   <structfield>relpages</structfield>.  We can look at it with
-   queries similar to this one:
+   <structfield>relpages</structfield> (except for global temporary relations,
+   for which it is kept in memory, and can be retrieved using
+   <link linkend="pg-gtr-info"><function>pg_gtr_info()</function></link>).
+   We can look at it with queries similar to this one:
 
 <screen>
 SELECT relname, relkind, reltuples, relpages
@@ -1317,7 +1319,7 @@ WHERE relname LIKE 'tenk1%';
    <structfield>reltuples</structfield> count on the basis of the part
    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>
+   will scale the latest statistics values
    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..ca61db63361 100644
--- a/doc/src/sgml/planstats.sgml
+++ b/doc/src/sgml/planstats.sgml
@@ -59,7 +59,23 @@ SELECT relpages, reltuples FROM pg_class WHERE relname = 'tenk1';
       358 |     10000
 </programlisting>
 
-    These numbers are current as of the last <command>VACUUM</command> or
+    The principle is the same for a global temporary table, except that the
+    relation statistics are held in memory, and must be retrieved using
+    <link linkend="pg-gtr-info"><function>pg_gtr_info()</function></link>:
+
+<programlisting>
+SELECT COALESCE(t.relpages, c.relpages) AS relpages,
+       COALESCE(t.reltuples, c.reltuples) AS reltuples
+FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+WHERE c.relname = 'gtt';
+
+ relpages | reltuples
+----------+-----------
+      265 |      7383
+</programlisting>
+
+    In either case, these numbers are current as of the last
+    <command>VACUUM</command> or
     <command>ANALYZE</command> on the table.  The planner then fetches the
     actual current number of pages in the table (this is a cheap operation,
     not requiring a table scan).  If that is different from
diff --git a/src/backend/catalog/global_temp.c b/src/backend/catalog/global_temp.c
index 81cb09cc4bc..66d242ccabc 100644
--- a/src/backend/catalog/global_temp.c
+++ b/src/backend/catalog/global_temp.c
@@ -1555,6 +1555,30 @@ GetGlobalTempRelationInfoForUpdate(Oid relid)
 	return &entry->history.info;
 }
 
+/*
+ * GetGlobalTempRelationInfoForInPlaceUpdate
+ *
+ *	Returns an in-place updatable copy of the session-local information held
+ *	for a global temporary relation used in the current session.  The return
+ *	value is guaranteed to be non-NULL (it is an error to call this for
+ *	anything other than an in-use global temporary relation).
+ *
+ *	The caller may directly edit any fields of the returned struct, but any
+ *	edits made are non-transactional, like systable_inplace_update_*().
+ */
+GtrInfo *
+GetGlobalTempRelationInfoForInPlaceUpdate(Oid relid)
+{
+	GtrInfo    *gtr_info;
+
+	/* Just return the current relation info, without saving a copy */
+	gtr_info = GetGlobalTempRelationInfo(relid);
+	if (gtr_info == NULL)
+		elog(ERROR, "cache lookup failed for global temp relation %u", relid);
+
+	return gtr_info;
+}
+
 /*
  * GetEffectivePgClassTuple
  *
@@ -1643,8 +1667,8 @@ pg_gtr_info(PG_FUNCTION_ARGS)
 	Oid			relid = PG_GETARG_OID(0);
 	TupleDesc	tupdesc;
 	GtrInfo    *gtr_info;
-	Datum		values[2];
-	bool		nulls[2];
+	Datum		values[6];
+	bool		nulls[6];
 
 	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
 		elog(ERROR, "return type must be a row type");
@@ -1655,6 +1679,10 @@ pg_gtr_info(PG_FUNCTION_ARGS)
 
 	values[0] = ObjectIdGetDatum(gtr_info->relfilenode);
 	values[1] = ObjectIdGetDatum(gtr_info->reltablespace);
+	values[2] = Int32GetDatum(gtr_info->relpages);
+	values[3] = Float4GetDatum(gtr_info->reltuples);
+	values[4] = Int32GetDatum(gtr_info->relallvisible);
+	values[5] = Int32GetDatum(gtr_info->relallfrozen);
 
 	memset(nulls, 0, sizeof(nulls));
 
@@ -1679,7 +1707,7 @@ pg_gtrs_in_use(PG_FUNCTION_ARGS)
 
 	if (gtr_local_usage != NULL)
 	{
-		bool		nulls[3];
+		bool		nulls[7];
 		HASH_SEQ_STATUS status;
 		GtrUsageEntry *entry;
 
@@ -1691,7 +1719,7 @@ pg_gtrs_in_use(PG_FUNCTION_ARGS)
 		while ((entry = hash_seq_search(&status)) != NULL)
 		{
 			GtrInfo    *gtr_info = &entry->history.info;
-			Datum		values[3];
+			Datum		values[7];
 
 			/* Ignore dropped relations */
 			if (entry->stopped_subid != InvalidSubTransactionId)
@@ -1700,6 +1728,10 @@ pg_gtrs_in_use(PG_FUNCTION_ARGS)
 			values[0] = ObjectIdGetDatum(entry->relid);
 			values[1] = ObjectIdGetDatum(gtr_info->relfilenode);
 			values[2] = ObjectIdGetDatum(gtr_info->reltablespace);
+			values[3] = Int32GetDatum(gtr_info->relpages);
+			values[4] = Float4GetDatum(gtr_info->reltuples);
+			values[5] = Int32GetDatum(gtr_info->relallvisible);
+			values[6] = Int32GetDatum(gtr_info->relallfrozen);
 
 			tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
 								 values, nulls);
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 16979fe54c3..44412fe12b1 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -3594,10 +3594,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;
 
@@ -3608,8 +3608,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
@@ -3651,13 +3651,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);
 	}
 
@@ -3672,7 +3682,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);
 	}
 }
@@ -3684,13 +3694,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.
@@ -3702,16 +3721,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 0b3f4dba922..746500f041e 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -124,6 +124,7 @@ static void UpdateIndexRelation(Oid indexoid, Oid heapoid,
 								bool isready,
 								char relpersistence);
 static void index_update_stats(Relation rel,
+							   bool isreindex,
 							   bool hasindex,
 							   double reltuples);
 static void IndexCheckExclusion(Relation heapRelation,
@@ -1298,6 +1299,7 @@ index_create(Relation heapRelation,
 		 * having an index.
 		 */
 		index_update_stats(heapRelation,
+						   false,
 						   true,
 						   -1.0);
 		/* Make the above update visible */
@@ -2849,18 +2851,24 @@ 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.
  *
+ * 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 the session-local GtrInfo
+ * struct. 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
@@ -2871,6 +2879,7 @@ FormIndexDatum(IndexInfo *indexInfo,
  */
 static void
 index_update_stats(Relation rel,
+				   bool isreindex,
 				   bool hasindex,
 				   double reltuples)
 {
@@ -2884,6 +2893,7 @@ index_update_stats(Relation rel,
 	HeapTuple	tuple;
 	void	   *state;
 	Form_pg_class rd_rel;
+	GtrInfo    *gtr_info;
 	bool		dirty;
 
 	/*
@@ -2968,28 +2978,50 @@ 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 this is a reindex on a global temporary table, we don't need to set
+	 * pg_class.relhasindex, and all other fields go in the table's GtrInfo,
+	 * so we only need a read-only copy of the pg_class tuple.  Otherwise, we
+	 * need a writable copy of the pg_class tuple.
 	 */
+	if (isreindex && RELATION_IS_GLOBAL_TEMP(rel))
+	{
+		pg_class = NULL;
+		tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+		state = NULL;
+	}
+	else
+	{
+		pg_class = table_open(RelationRelationId, RowExclusiveLock);
 
-	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);
+		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);
 	rd_rel = (Form_pg_class) GETSTRUCT(tuple);
 
+	/*
+	 * For a global temporary relation, do an in-place update of its GtrInfo
+	 * so that it behaves the same as a permanent relation.
+	 */
+	if (RELATION_IS_GLOBAL_TEMP(rel))
+		gtr_info = GetGlobalTempRelationInfoForInPlaceUpdate(relid);
+	else
+		gtr_info = NULL;
+
 	/* 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 / gtr_info */
 
 	dirty = false;
-	if (rd_rel->relhasindex != hasindex)
+	if (RelationIsValid(pg_class) && rd_rel->relhasindex != hasindex)
 	{
 		rd_rel->relhasindex = hasindex;
 		dirty = true;
@@ -2997,30 +3029,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, gtr_info, (int32) relpages,
+							  &dirty, NULL);
+		SetEffective_reltuples(rd_rel, gtr_info, (float4) reltuples,
+							   &dirty, NULL);
+		SetEffective_relallvisible(rd_rel, gtr_info, (int32) relallvisible,
+								   &dirty, NULL);
+		SetEffective_relallfrozen(rd_rel, gtr_info, (int32) relallfrozen,
+								  &dirty, NULL);
 	}
 
 	/*
-	 * If anything changed, write out the tuple
+	 * If anything in pg_class changed, write out the tuple
 	 */
 	if (dirty)
 	{
@@ -3029,7 +3049,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
@@ -3043,7 +3064,8 @@ index_update_stats(Relation rel,
 
 	heap_freetuple(tuple);
 
-	table_close(pg_class, RowExclusiveLock);
+	if (RelationIsValid(pg_class))
+		table_close(pg_class, RowExclusiveLock);
 }
 
 
@@ -3221,11 +3243,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/commands/repack.c b/src/backend/commands/repack.c
index 6da84baed44..398049d1e98 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -1388,6 +1388,7 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 	Relation	relRelation;
 	HeapTuple	reltup;
 	Form_pg_class relform;
+	GtrInfo    *gtr_info;
 	TupleDesc	oldTupDesc PG_USED_FOR_ASSERTS_ONLY;
 	TupleDesc	newTupDesc PG_USED_FOR_ASSERTS_ONLY;
 	VacuumParams params;
@@ -1583,7 +1584,10 @@ 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 or the session-local GtrInfo struct to reflect the
+	 * correct values of pages and tuples.
+	 */
 	relRelation = table_open(RelationRelationId, RowExclusiveLock);
 
 	reltup = SearchSysCacheCopy1(RELOID,
@@ -1593,11 +1597,17 @@ copy_table_data(Relation NewHeap, Relation OldHeap, Relation OldIndex,
 			 RelationGetRelid(NewHeap));
 	relform = (Form_pg_class) GETSTRUCT(reltup);
 
-	relform->relpages = num_pages;
-	relform->reltuples = num_tuples;
+	if (RELATION_IS_GLOBAL_TEMP(NewHeap))
+		gtr_info = GetGlobalTempRelationInfoForUpdate(RelationGetRelid(NewHeap));
+	else
+		gtr_info = NULL;
+
+	SetEffective_relpages(relform, gtr_info, num_pages, NULL, NULL);
+	SetEffective_reltuples(relform, gtr_info, num_tuples, NULL, NULL);
 
 	/* Don't update the stats for pg_class.  See swap_relation_files. */
-	if (RelationGetRelid(OldHeap) != RelationRelationId)
+	if (!RELATION_IS_GLOBAL_TEMP(NewHeap) &&
+		RelationGetRelid(OldHeap) != RelationRelationId)
 		CatalogTupleUpdate(relRelation, &reltup->t_self, reltup);
 	else
 		CacheInvalidateRelcacheByTuple(reltup);
@@ -1836,21 +1846,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, gtr_info1);
+		SetEffective_relpages(relform1, gtr_info1,
+							  GetEffective_relpages(relform2, gtr_info2),
+							  NULL, NULL);
+		SetEffective_relpages(relform2, gtr_info2, swap_pages, NULL, NULL);
+
+		swap_tuples = GetEffective_reltuples(relform1, gtr_info1);
+		SetEffective_reltuples(relform1, gtr_info1,
+							   GetEffective_reltuples(relform2, gtr_info2),
+							   NULL, NULL);
+		SetEffective_reltuples(relform2, gtr_info2, swap_tuples, NULL, NULL);
+
+		swap_allvisible = GetEffective_relallvisible(relform1, gtr_info1);
+		SetEffective_relallvisible(relform1, gtr_info1,
+								   GetEffective_relallvisible(relform2, gtr_info2),
+								   NULL, NULL);
+		SetEffective_relallvisible(relform2, gtr_info2, swap_allvisible, NULL, NULL);
+
+		swap_allfrozen = GetEffective_relallfrozen(relform1, gtr_info1);
+		SetEffective_relallfrozen(relform1, gtr_info1,
+								  GetEffective_relallfrozen(relform2, gtr_info2),
+								  NULL, NULL);
+		SetEffective_relallfrozen(relform2, gtr_info2, swap_allfrozen, NULL, NULL);
 	}
 
 	/*
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index b6205a1dd40..758cf98b692 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -60,6 +60,7 @@
 #include "utils/guc.h"
 #include "utils/guc_hooks.h"
 #include "utils/injection_point.h"
+#include "utils/inval.h"
 #include "utils/memutils.h"
 #include "utils/snapmgr.h"
 #include "utils/syscache.h"
@@ -1418,7 +1419,8 @@ 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
+ *		row (and the session-local GtrInfo struct, 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.
  *
@@ -1470,7 +1472,9 @@ vac_update_relstats(Relation relation,
 	HeapTuple	ctup;
 	void	   *inplace_state;
 	Form_pg_class pgcform;
+	GtrInfo    *gtr_info;
 	bool		dirty,
+				gtr_dirty,
 				futurexid,
 				futuremxid;
 	TransactionId oldfrozenxid;
@@ -1490,29 +1494,27 @@ vac_update_relstats(Relation relation,
 			 relid);
 	pgcform = (Form_pg_class) GETSTRUCT(ctup);
 
-	/* Apply statistical updates, if any, to copied tuple */
+	/*
+	 * For a global temporary relation, do an in-place update of its GtrInfo
+	 * so that it behaves the same as a permanent relation.
+	 */
+	if (RELATION_IS_GLOBAL_TEMP(relation))
+		gtr_info = GetGlobalTempRelationInfoForInPlaceUpdate(relid);
+	else
+		gtr_info = NULL;
+
+	/* 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;
-	}
+	gtr_dirty = false;
+	SetEffective_relpages(pgcform, gtr_info, (int32) num_pages,
+						  &dirty, &gtr_dirty);
+	SetEffective_reltuples(pgcform, gtr_info, (float4) num_tuples,
+						   &dirty, &gtr_dirty);
+	SetEffective_relallvisible(pgcform, gtr_info, (int32) num_all_visible_pages,
+							   &dirty, &gtr_dirty);
+	SetEffective_relallfrozen(pgcform, gtr_info, (int32) num_all_frozen_pages,
+							  &dirty, &gtr_dirty);
 
 	/* Apply DDL updates, but not inside an outer transaction (see above) */
 
@@ -1595,12 +1597,24 @@ vac_update_relstats(Relation relation,
 		}
 	}
 
-	/* If anything changed, write out the tuple. */
+	/* If anything in pg_class changed, write out the tuple */
 	if (dirty)
+	{
 		systable_inplace_update_finish(inplace_state, ctup);
+		/* the above sends transactional and immediate cache inval messages */
+	}
 	else
+	{
 		systable_inplace_update_cancel(inplace_state);
 
+		/*
+		 * If anything changed in a global temporary relation, we must also do
+		 * a relcache inval, to cause the new values to be loaded.
+		 */
+		if (gtr_dirty)
+			CacheInvalidateRelcacheByTuple(ctup);
+	}
+
 	table_close(rd, RowExclusiveLock);
 
 	if (futurexid)
diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index f2743c00c58..fcd125f2a63 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -20,6 +20,7 @@
 #include <math.h>
 
 #include "access/heapam.h"
+#include "catalog/global_temp.h"
 #include "catalog/indexing.h"
 #include "catalog/namespace.h"
 #include "nodes/makefuncs.h"
@@ -110,10 +111,8 @@ relation_statistics_update_internal(Oid reloid, FunctionCallInfo fcinfo)
 	Relation	crel;
 	HeapTuple	ctup;
 	Form_pg_class pgcform;
-	int			replaces[4] = {0};
-	Datum		values[4] = {0};
-	bool		nulls[4] = {0};
-	int			nreplaces = 0;
+	GtrInfo    *gtr_info;
+	bool		dirty;
 	bool		result = true;
 
 	if (!PG_ARGISNULL(RELPAGES_ARG))
@@ -161,52 +160,46 @@ relation_statistics_update_internal(Oid reloid, FunctionCallInfo fcinfo)
 	 */
 	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 session-local GtrInfo
+	 * struct.  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++;
+		gtr_info = GetGlobalTempRelationInfoForUpdate(reloid);
 	}
+	else
+		gtr_info = NULL;
 
-	if (update_relallfrozen && relallfrozen != pgcform->relallfrozen)
-	{
-		replaces[nreplaces] = Anum_pg_class_relallfrozen;
-		values[nreplaces] = Int32GetDatum(relallfrozen);
-		nreplaces++;
-	}
+	dirty = false;
 
-	if (nreplaces > 0)
-	{
-		TupleDesc	tupdesc = RelationGetDescr(crel);
-		HeapTuple	newtup;
+	if (update_relpages)
+		SetEffective_relpages(pgcform, gtr_info, relpages, &dirty, NULL);
 
-		newtup = heap_modify_tuple_by_cols(ctup, tupdesc, nreplaces,
-										   replaces, values, nulls);
-		CatalogTupleUpdate(crel, &newtup->t_self, newtup);
-		heap_freetuple(newtup);
-	}
+	if (update_reltuples)
+		SetEffective_reltuples(pgcform, gtr_info, reltuples, &dirty, NULL);
+
+	if (update_relallvisible)
+		SetEffective_relallvisible(pgcform, gtr_info, relallvisible, &dirty, NULL);
+
+	if (update_relallfrozen)
+		SetEffective_relallfrozen(pgcform, gtr_info, relallfrozen, &dirty, NULL);
+
+	if (dirty)
+		CatalogTupleUpdate(crel, &ctup->t_self, 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/relcache.c b/src/backend/utils/cache/relcache.c
index abcb038747c..dbc139aac92 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -4033,10 +4033,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, gtr_info, 0, NULL, NULL);
+			SetEffective_reltuples(classform, gtr_info, -1, NULL, NULL);
+			SetEffective_relallvisible(classform, gtr_info, 0, NULL, NULL);
+			SetEffective_relallfrozen(classform, gtr_info, 0, NULL, NULL);
 		}
 		classform->relfrozenxid = freezeXid;
 		classform->relminmxid = minmulti;
diff --git a/src/include/catalog/global_temp.h b/src/include/catalog/global_temp.h
index 10e5ba6d261..584e219b24f 100644
--- a/src/include/catalog/global_temp.h
+++ b/src/include/catalog/global_temp.h
@@ -29,6 +29,10 @@ typedef struct GtrInfo
 	/* pg_class info */
 	Oid			relfilenode;	/* the relation's physical storage file */
 	Oid			reltablespace;	/* the relation's tablespace identifier */
+	int32		relpages;		/* rel stats: number of blocks */
+	float4		reltuples;		/* rel stats: number of tuples */
+	int32		relallvisible;	/* rel stats: number of all-visible blocks */
+	int32		relallfrozen;	/* rel stats: number of all-frozen blocks */
 
 	/* pg_index info */
 	bool		indisvalid;		/* is the index valid in this session? */
@@ -45,6 +49,10 @@ typedef struct GtrInfo
 	do { \
 		(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)
 
 extern void TrackGlobalTempRelationStorage(Oid relid, RelFileLocator rlocator,
@@ -66,6 +74,7 @@ extern bool IsOtherUsingGlobalTempRelation(Oid relid);
 extern List *GetAllGlobalTempRelationsInUse(Oid dbId);
 extern GtrInfo *GetGlobalTempRelationInfo(Oid relid);
 extern GtrInfo *GetGlobalTempRelationInfoForUpdate(Oid relid);
+extern GtrInfo *GetGlobalTempRelationInfoForInPlaceUpdate(Oid relid);
 extern HeapTuple GetEffectivePgClassTuple(Oid relid);
 extern HeapTuple GetEffectivePgIndexTuple(Oid indexrelid);
 
@@ -89,6 +98,46 @@ GetEffective_reltablespace(Form_pg_class class_form, GtrInfo *gtr_info)
 	return gtr_info != NULL ? gtr_info->reltablespace : class_form->reltablespace;
 }
 
+/*
+ * Get the effective value of relpages for a relation.  For a global temporary
+ * relation, the value from gtr_info (if present) takes precedence.
+ */
+static inline int32
+GetEffective_relpages(Form_pg_class class_form, GtrInfo *gtr_info)
+{
+	return gtr_info != NULL ? gtr_info->relpages : class_form->relpages;
+}
+
+/*
+ * Get the effective value of reltuples for a relation.  For a global
+ * temporary relation, the value from gtr_info (if present) takes precedence.
+ */
+static inline float4
+GetEffective_reltuples(Form_pg_class class_form, GtrInfo *gtr_info)
+{
+	return gtr_info != NULL ? gtr_info->reltuples : class_form->reltuples;
+}
+
+/*
+ * Get the effective value of relallvisible for a relation.  For a global
+ * temporary relation, the value from gtr_info (if present) takes precedence.
+ */
+static inline int32
+GetEffective_relallvisible(Form_pg_class class_form, GtrInfo *gtr_info)
+{
+	return gtr_info != NULL ? gtr_info->relallvisible : class_form->relallvisible;
+}
+
+/*
+ * Get the effective value of relallfrozen for a relation.  For a global
+ * temporary relation, the value from gtr_info (if present) takes precedence.
+ */
+static inline int32
+GetEffective_relallfrozen(Form_pg_class class_form, GtrInfo *gtr_info)
+{
+	return gtr_info != NULL ? gtr_info->relallfrozen : class_form->relallfrozen;
+}
+
 /*
  * Get the effective value of indisvalid for an index relation.  For a global
  * temporary relation, the value from gtr_info (if present) takes precedence.
@@ -130,4 +179,124 @@ SetEffective_reltablespace(Form_pg_class class_form, GtrInfo *gtr_info, Oid newv
 	class_form->reltablespace = newval;
 }
 
+/*
+ * Set the effective value of relpages for a relation.  For a global temporary
+ * relation, GetGlobalTempRelationInfoFor[InPlace]Update() should have been
+ * used to obtain gtr_info, and it will be updated instead of the pg_class
+ * entry.  Otherwise, the value is set in the pg_class entry.
+ *
+ * If non-NULL, the class_dirty or gtr_dirty flag is set to true, if the value
+ * in pg_class or gtr_info actually changes.
+ */
+static inline void
+SetEffective_relpages(Form_pg_class class_form, GtrInfo *gtr_info,
+					  int32 newval, bool *class_dirty, bool *gtr_dirty)
+{
+	if (gtr_info != NULL)
+	{
+		if (newval != gtr_info->relpages)
+		{
+			gtr_info->relpages = newval;
+			if (gtr_dirty != NULL)
+				*gtr_dirty = true;
+		}
+	}
+	else if (newval != class_form->relpages)
+	{
+		class_form->relpages = newval;
+		if (class_dirty != NULL)
+			*class_dirty = true;
+	}
+}
+
+/*
+ * Set the effective value of reltuples for a relation.  For a global
+ * temporary relation, GetGlobalTempRelationInfoFor[InPlace]Update() should
+ * have been used to obtain gtr_info, and it will be updated instead of the
+ * pg_class entry.  Otherwise, the value is set in the pg_class entry.
+ *
+ * If non-NULL, the class_dirty or gtr_dirty flag is set to true, if the value
+ * in pg_class or gtr_info actually changes.
+ */
+static inline void
+SetEffective_reltuples(Form_pg_class class_form, GtrInfo *gtr_info,
+					   float4 newval, bool *class_dirty, bool *gtr_dirty)
+{
+	if (gtr_info != NULL)
+	{
+		if (newval != gtr_info->reltuples)
+		{
+			gtr_info->reltuples = newval;
+			if (gtr_dirty != NULL)
+				*gtr_dirty = true;
+		}
+	}
+	else if (newval != class_form->reltuples)
+	{
+		class_form->reltuples = newval;
+		if (class_dirty != NULL)
+			*class_dirty = true;
+	}
+}
+
+/*
+ * Set the effective value of relallvisible for a relation.  For a global
+ * temporary relation, GetGlobalTempRelationInfoFor[InPlace]Update() should
+ * have been used to obtain gtr_info, and it will be updated instead of the
+ * pg_class entry.  Otherwise, the value is set in the pg_class entry.
+ *
+ * If non-NULL, the class_dirty or gtr_dirty flag is set to true, if the value
+ * in pg_class or gtr_info actually changes.
+ */
+static inline void
+SetEffective_relallvisible(Form_pg_class class_form, GtrInfo *gtr_info,
+						   int32 newval, bool *class_dirty, bool *gtr_dirty)
+{
+	if (gtr_info != NULL)
+	{
+		if (newval != gtr_info->relallvisible)
+		{
+			gtr_info->relallvisible = newval;
+			if (gtr_dirty != NULL)
+				*gtr_dirty = true;
+		}
+	}
+	else if (newval != class_form->relallvisible)
+	{
+		class_form->relallvisible = newval;
+		if (class_dirty != NULL)
+			*class_dirty = true;
+	}
+}
+
+/*
+ * Set the effective value of relallfrozen for a relation.  For a global
+ * temporary relation, GetGlobalTempRelationInfoFor[InPlace]Update() should
+ * have been used to obtain gtr_info, and it will be updated instead of the
+ * pg_class entry.  Otherwise, the value is set in the pg_class entry.
+ *
+ * If non-NULL, the class_dirty or gtr_dirty flag is set to true, if the value
+ * in pg_class or gtr_info actually changes.
+ */
+static inline void
+SetEffective_relallfrozen(Form_pg_class class_form, GtrInfo *gtr_info,
+						  int32 newval, bool *class_dirty, bool *gtr_dirty)
+{
+	if (gtr_info != NULL)
+	{
+		if (newval != gtr_info->relallfrozen)
+		{
+			gtr_info->relallfrozen = newval;
+			if (gtr_dirty != NULL)
+				*gtr_dirty = true;
+		}
+	}
+	else if (newval != class_form->relallfrozen)
+	{
+		class_form->relallfrozen = newval;
+		if (class_dirty != NULL)
+			*class_dirty = true;
+	}
+}
+
 #endif							/* GLOBAL_TEMP_H */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 852654de5b8..36668b728bd 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12782,16 +12782,16 @@
 { oid => '8082', descr => 'get information about an in-use global temporary relation',
   proname => 'pg_gtr_info', provolatile => 's', proparallel => 'u',
   proargtypes => 'oid', prorettype => 'record',
-  proallargtypes => '{oid,oid,oid}',
-  proargmodes => '{i,o,o}',
-  proargnames => '{oid,relfilenode,reltablespace}',
+  proallargtypes => '{oid,oid,oid,int4,float4,int4,int4}',
+  proargmodes => '{i,o,o,o,o,o,o}',
+  proargnames => '{oid,relfilenode,reltablespace,relpages,reltuples,relallvisible,relallfrozen}',
   prosrc => 'pg_gtr_info' },
 { oid => '8083', descr => 'get information about all in-use global temporary relations',
   proname => 'pg_gtrs_in_use', provolatile => 's', proparallel => 'u',
   proargtypes => '', proretset => 't', prorettype => 'record', prorows => '10',
-  proallargtypes => '{oid,oid,oid}',
-  proargmodes => '{o,o,o}',
-  proargnames => '{oid,relfilenode,reltablespace}',
+  proallargtypes => '{oid,oid,oid,int4,float4,int4,int4}',
+  proargmodes => '{o,o,o,o,o,o,o}',
+  proargnames => '{oid,relfilenode,reltablespace,relpages,reltuples,relallvisible,relallfrozen}',
   prosrc => 'pg_gtrs_in_use' },
 { oid => '8084', descr => 'is an in-use global temporary index relation valid?',
   proname => 'pg_gtr_index_is_valid', provolatile => 's', proparallel => 'u',
diff --git a/src/test/isolation/expected/global-temp.out b/src/test/isolation/expected/global-temp.out
index 27a7fe28a52..f79cf24e1c4 100644
--- a/src/test/isolation/expected/global-temp.out
+++ b/src/test/isolation/expected/global-temp.out
@@ -299,6 +299,38 @@ t         |t
 
 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 300a0314e8e..56534154590 100644
--- a/src/test/isolation/specs/global-temp.spec
+++ b/src/test/isolation/specs/global-temp.spec
@@ -21,6 +21,7 @@ step drop_tblspace { DROP TABLESPACE regress_isolation_tablespace; }
 
 # Transaction control
 step b1 { BEGIN; }
+step c1 { COMMIT; }
 step r1 { ROLLBACK; }
 step sp1 { SAVEPOINT sp; }
 step rsp1 { ROLLBACK TO SAVEPOINT sp; }
@@ -53,8 +54,9 @@ step idx_valid1 {
     FROM pg_index WHERE indexrelid = 'tmp2_un'::regclass;
 }
 
-# Test DROP with ON COMMIT DELETE ROWS
+# Test concurrent ON COMMIT DELETE ROWS
 step create1dr { CREATE GLOBAL TEMP TABLE tmp2 (key int, val text) ON COMMIT DELETE ROWS; }
+step sel1_2 { SELECT * FROM tmp2; }
 
 # Test local TRUNCATE
 step t1 { TRUNCATE tmp; }
@@ -113,6 +115,9 @@ step idx_valid2 {
     FROM pg_index WHERE indexrelid = 'tmp2_un'::regclass;
 }
 
+# Test concurrent ON COMMIT DELETE ROWS
+step sel2_2 { SELECT * FROM tmp2; }
+
 # Test GTT inval in prepared transaction
 step drop2 { DROP TABLE tmp2; }
 
@@ -164,6 +169,9 @@ permutation create1 ins1_2 ins2_2
             uniq_idx1 seltype1 seltype2
             idx_valid1 idx_valid2 uniq_reidx2 idx_valid2 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 abbd5cde670..88c80f4884c 100644
--- a/src/test/regress/expected/global_temp.out
+++ b/src/test/regress/expected/global_temp.out
@@ -71,14 +71,14 @@ SELECT * FROM tmp1;
 \c
 SET search_path = global_temp_tests;
 SELECT * FROM pg_gtr_info('tmp1'::regclass);
- relfilenode | reltablespace 
--------------+---------------
-             |              
+ relfilenode | reltablespace | relpages | reltuples | relallvisible | relallfrozen 
+-------------+---------------+----------+-----------+---------------+--------------
+             |               |          |           |               |             
 (1 row)
 
 SELECT * FROM pg_gtrs_in_use();
- oid | relfilenode | reltablespace 
------+-------------+---------------
+ oid | relfilenode | reltablespace | relpages | reltuples | relallvisible | relallfrozen 
+-----+-------------+---------------+----------+-----------+---------------+--------------
 (0 rows)
 
 SELECT * FROM tmp1;
@@ -89,23 +89,31 @@ SELECT * FROM tmp1;
 SELECT c.relfilenode = c.oid,
        pg_relation_filenode('tmp1'::regclass) = c.relfilenode,
        t.relfilenode = c.relfilenode,
-       t.reltablespace = c.reltablespace
+       t.reltablespace = c.reltablespace,
+       t.relpages = c.relpages,
+       t.reltuples = c.reltuples,
+       t.relallvisible = c.relallvisible,
+       t.relallfrozen = c.relallfrozen
   FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
  WHERE c.oid = 'tmp1'::regclass;
- ?column? | ?column? | ?column? | ?column? 
-----------+----------+----------+----------
- t        | t        | t        | t
+ ?column? | ?column? | ?column? | ?column? | ?column? | ?column? | ?column? | ?column? 
+----------+----------+----------+----------+----------+----------+----------+----------
+ t        | t        | t        | t        | t        | t        | t        | t
 (1 row)
 
 SELECT c.relname,
        t.relfilenode = c.relfilenode,
-       t.reltablespace = c.reltablespace
+       t.reltablespace = c.reltablespace,
+       t.relpages = c.relpages,
+       t.reltuples = c.reltuples,
+       t.relallvisible = c.relallvisible,
+       t.relallfrozen = c.relallfrozen
   FROM pg_gtrs_in_use() t LEFT JOIN pg_class c ON c.oid = t.oid
  ORDER BY c.relname;
-  relname  | ?column? | ?column? 
------------+----------+----------
- tmp1      | t        | t
- tmp1_pkey | t        | t
+  relname  | ?column? | ?column? | ?column? | ?column? | ?column? | ?column? 
+-----------+----------+----------+----------+----------+----------+----------
+ tmp1      | t        | t        | t        | t        | t        | t
+ tmp1_pkey | t        | t        | t        | t        | t        | t
 (2 rows)
 
 -- Test index
@@ -776,3 +784,213 @@ SELECT oid::regclass FROM pg_gtrs_in_use();
 -----
 (0 rows)
 
+-- 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, LATERAL pg_gtr_info(c.oid) t
+ 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, LATERAL pg_gtr_info(c.oid) t
+ 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, LATERAL pg_gtr_info(c.oid) t
+ 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, LATERAL pg_gtr_info(c.oid) t
+ 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, LATERAL pg_gtr_info(c.oid) t
+ 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_gtr_info('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_gtr_info('tmp2'::regclass);
+ reltuples 
+-----------
+       100
+(1 row)
+
+SELECT row_estimate('SELECT * FROM tmp2');
+ row_estimate 
+--------------
+          100
+(1 row)
+
+ROLLBACK;
+SELECT reltuples FROM pg_gtr_info('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_gtr_info('tmp2'::regclass);
+ reltuples 
+-----------
+        50
+(1 row)
+
+SELECT row_estimate('SELECT * FROM tmp2');
+ row_estimate 
+--------------
+           50
+(1 row)
+
+ROLLBACK;
+SELECT reltuples FROM pg_gtr_info('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, t.relpages, t.reltuples, t.relallvisible, t.relallfrozen
+  FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+ WHERE c.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, t.relpages, t.reltuples, t.relallvisible, t.relallfrozen
+  FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+ WHERE c.oid = 'tmp2'::regclass;
+ oid  | relpages | reltuples | relallvisible | relallfrozen 
+------+----------+-----------+---------------+--------------
+ tmp2 |        5 |       150 |            10 |           20
+(1 row)
+
+DROP TABLE tmp2;
diff --git a/src/test/regress/sql/global_temp.sql b/src/test/regress/sql/global_temp.sql
index af0073ad242..85b451d4cac 100644
--- a/src/test/regress/sql/global_temp.sql
+++ b/src/test/regress/sql/global_temp.sql
@@ -46,13 +46,21 @@ SELECT * FROM tmp1;
 SELECT c.relfilenode = c.oid,
        pg_relation_filenode('tmp1'::regclass) = c.relfilenode,
        t.relfilenode = c.relfilenode,
-       t.reltablespace = c.reltablespace
+       t.reltablespace = c.reltablespace,
+       t.relpages = c.relpages,
+       t.reltuples = c.reltuples,
+       t.relallvisible = c.relallvisible,
+       t.relallfrozen = c.relallfrozen
   FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
  WHERE c.oid = 'tmp1'::regclass;
 
 SELECT c.relname,
        t.relfilenode = c.relfilenode,
-       t.reltablespace = c.reltablespace
+       t.reltablespace = c.reltablespace,
+       t.relpages = c.relpages,
+       t.reltuples = c.reltuples,
+       t.relallvisible = c.relallvisible,
+       t.relallfrozen = c.relallfrozen
   FROM pg_gtrs_in_use() t LEFT JOIN pg_class c ON c.oid = t.oid
  ORDER BY c.relname;
 
@@ -401,3 +409,114 @@ CREATE GLOBAL TEMP SEQUENCE s2;
 SELECT oid::regclass FROM pg_gtrs_in_use();
 ROLLBACK;
 SELECT oid::regclass FROM pg_gtrs_in_use();
+
+-- 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, LATERAL pg_gtr_info(c.oid) t
+ 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, LATERAL pg_gtr_info(c.oid) t
+ 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, LATERAL pg_gtr_info(c.oid) t
+ 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, LATERAL pg_gtr_info(c.oid) t
+ 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, LATERAL pg_gtr_info(c.oid) t
+ 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_gtr_info('tmp2'::regclass);
+SELECT row_estimate('SELECT * FROM tmp2');
+
+BEGIN;
+INSERT INTO tmp2 SELECT * FROM generate_series(1, 100);
+ANALYZE tmp2;
+SELECT reltuples FROM pg_gtr_info('tmp2'::regclass);
+SELECT row_estimate('SELECT * FROM tmp2');
+ROLLBACK;
+
+SELECT reltuples FROM pg_gtr_info('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_gtr_info('tmp2'::regclass);
+SELECT row_estimate('SELECT * FROM tmp2');
+ROLLBACK;
+
+SELECT reltuples FROM pg_gtr_info('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, t.relpages, t.reltuples, t.relallvisible, t.relallfrozen
+  FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+ WHERE c.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, t.relpages, t.reltuples, t.relallvisible, t.relallfrozen
+  FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+ WHERE c.oid = 'tmp2'::regclass;
+
+DROP TABLE tmp2;
-- 
2.51.0

