From 3d19af0b9437e87bd914507e784706669ad00e21 Mon Sep 17 00:00:00 2001
From: Dean Rasheed <dean.a.rasheed@gmail.com>
Date: Wed, 17 Jun 2026 10:04:41 +0100
Subject: [PATCH v11 6/9] Support local vacuuming of global temporary tables.

The per-table VACUUM results (relfrozenxid and relminmxid) are stored
in memory for each global temporary table, instead of writing them to
pg_class. The corresponding pg_class fields are kept invalid (zero).

Additionally, each session stores its minimum relfrozenxid and
relminmxid values over all in-use global temporary tables as
tempfrozenxid and tempminmxid in its PGPROC struct, allowing other
sessions to advance datfrozenxid and datminmxid, taking into account
these minimum frozen XIDs from other sessions. This allows VACUUM to
keep advancing datfrozenxid and datminmxid as long as each session
that uses global temporary tables runs VACUUM from time to time.

To help administrators monitor this, each backend's tempfrozenxid and
tempminmxid values are accessible via the pg_stat_activity view.
---
 doc/src/sgml/catalogs.sgml                    |  12 ++
 doc/src/sgml/config.sgml                      |  12 +-
 doc/src/sgml/func/func-info.sgml              |   8 +-
 doc/src/sgml/maintenance.sgml                 |  35 ++++-
 doc/src/sgml/monitoring.sgml                  |  28 ++++
 src/backend/access/heap/vacuumlazy.c          |   6 +-
 src/backend/access/transam/twophase.c         |   3 +
 src/backend/catalog/global_temp.c             | 106 ++++++++++++-
 src/backend/catalog/heap.c                    |  14 +-
 src/backend/catalog/system_views.sql          |   2 +
 src/backend/commands/repack.c                 |  23 ++-
 src/backend/commands/vacuum.c                 | 141 +++++++++++++++--
 src/backend/storage/lmgr/proc.c               |   7 +
 src/backend/utils/adt/pgstatfuncs.c           |  41 ++++-
 src/backend/utils/cache/relcache.c            |  25 ++-
 src/include/catalog/global_temp.h             |  85 +++++++++++
 src/include/catalog/pg_proc.dat               |  18 +--
 src/include/commands/vacuum.h                 |   2 +-
 src/include/storage/proc.h                    |   9 ++
 .../isolation/expected/vacuum-global-temp.out | 127 ++++++++++++++++
 src/test/isolation/isolation_schedule         |   1 +
 .../isolation/specs/vacuum-global-temp.spec   |  75 +++++++++
 src/test/regress/expected/global_temp.out     | 143 ++++++++++++++++--
 src/test/regress/expected/rules.out           |  10 +-
 src/test/regress/sql/global_temp.sql          |  79 +++++++++-
 25 files changed, 932 insertions(+), 80 deletions(-)
 create mode 100644 src/test/isolation/expected/vacuum-global-temp.out
 create mode 100644 src/test/isolation/specs/vacuum-global-temp.spec

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index f06be5883f6..b3df993317b 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2337,6 +2337,12 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
        whether the table needs to be vacuumed in order to prevent transaction
        ID wraparound or to allow <literal>pg_xact</literal> to be shrunk.  Zero
        (<symbol>InvalidTransactionId</symbol>) if the relation is not a table.
+      </para>
+      <para>
+       For a global temporary relation, this value is always zero, and the
+       <structfield>relfrozenxid</structfield> value returned by
+       <link linkend="pg-gtr-info"><function>pg_gtr_info()</function></link>,
+       if non-null, is the actual value for the current session.
       </para></entry>
      </row>
 
@@ -2350,6 +2356,12 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
        whether the table needs to be vacuumed in order to prevent multixact ID
        wraparound or to allow <literal>pg_multixact</literal> to be shrunk.  Zero
        (<symbol>InvalidMultiXactId</symbol>) if the relation is not a table.
+      </para>
+      <para>
+       For a global temporary relation, this value is always zero, and the
+       <structfield>relminmxid</structfield> value returned by
+       <link linkend="pg-gtr-info"><function>pg_gtr_info()</function></link>,
+       if non-null, is the actual value for the current session.
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 0165eb9ec02..56ad3b6b5da 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -10067,7 +10067,8 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
        <listitem>
         <para>
          <command>VACUUM</command> performs an aggressive scan if the table's
-         <structname>pg_class</structname>.<structfield>relfrozenxid</structfield> field has reached
+         <structname>pg_class</structname>.<structfield>relfrozenxid</structfield>
+         field (or the in-memory value held for a global temporary table) has reached
          the age specified by this setting.  An aggressive scan differs from
          a regular <command>VACUUM</command> in that it visits every page that might
          contain unfrozen XIDs or MXIDs, not just those that might contain dead
@@ -10115,7 +10116,8 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         <para>
          Specifies the maximum age (in transactions) that a table's
          <structname>pg_class</structname>.<structfield>relfrozenxid</structfield>
-         field can attain before <command>VACUUM</command> takes
+         field (or the in-memory value held for a global temporary table)
+         can attain before <command>VACUUM</command> takes
          extraordinary measures to avoid system-wide transaction ID
          wraparound failure.  This is <command>VACUUM</command>'s
          strategy of last resort.  The failsafe typically triggers
@@ -10151,7 +10153,8 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
        <listitem>
         <para>
          <command>VACUUM</command> performs an aggressive scan if the table's
-         <structname>pg_class</structname>.<structfield>relminmxid</structfield> field has reached
+         <structname>pg_class</structname>.<structfield>relminmxid</structfield>
+         field (or the in-memory value held for a global temporary table) has reached
          the age specified by this setting.  An aggressive scan differs from
          a regular <command>VACUUM</command> in that it visits every page that might
          contain unfrozen XIDs or MXIDs, not just those that might contain dead
@@ -10197,7 +10200,8 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         <para>
          Specifies the maximum age (in multixacts) that a table's
          <structname>pg_class</structname>.<structfield>relminmxid</structfield>
-         field can attain before <command>VACUUM</command> takes
+         field (or the in-memory value held for a global temporary table)
+         can attain before <command>VACUUM</command> takes
          extraordinary measures to avoid system-wide multixact ID
          wraparound failure.  This is <command>VACUUM</command>'s
          strategy of last resort.  The failsafe typically triggers when
diff --git a/doc/src/sgml/func/func-info.sgml b/doc/src/sgml/func/func-info.sgml
index bfc2e4d7811..5809552a17d 100644
--- a/doc/src/sgml/func/func-info.sgml
+++ b/doc/src/sgml/func/func-info.sgml
@@ -376,7 +376,9 @@
         <parameter>relpages</parameter> <type>int4</type>,
         <parameter>reltuples</parameter> <type>float4</type>,
         <parameter>relallvisible</parameter> <type>int4</type>,
-        <parameter>relallfrozen</parameter> <type>int4</type> )
+        <parameter>relallfrozen</parameter> <type>int4</type>,
+        <parameter>relfrozenxid</parameter> <type>xid</type>,
+        <parameter>relminmxid</parameter> <type>xid</type> )
        </para>
        <para>
         Returns information about a global temporary relation being used in
@@ -405,7 +407,9 @@
         <parameter>relpages</parameter> <type>int4</type>,
         <parameter>reltuples</parameter> <type>float4</type>,
         <parameter>relallvisible</parameter> <type>int4</type>,
-        <parameter>relallfrozen</parameter> <type>int4</type> )
+        <parameter>relallfrozen</parameter> <type>int4</type>,
+        <parameter>relfrozenxid</parameter> <type>xid</type>,
+        <parameter>relminmxid</parameter> <type>xid</type> )
        </para>
        <para>
         Returns information about all global temporary relations being used in
diff --git a/doc/src/sgml/maintenance.sgml b/doc/src/sgml/maintenance.sgml
index 7ceb7c78a10..ffc3b0a8a0e 100644
--- a/doc/src/sgml/maintenance.sgml
+++ b/doc/src/sgml/maintenance.sgml
@@ -609,16 +609,27 @@
     To track the age of the oldest unfrozen XIDs in a database,
     <command>VACUUM</command> stores XID
     statistics in the system tables <structname>pg_class</structname> and
-    <structname>pg_database</structname>.  In particular,
-    the <structfield>relfrozenxid</structfield> column of a table's
-    <structname>pg_class</structname> row contains the oldest remaining unfrozen
+    <structname>pg_database</structname>, as well as keeping in-memory
+    statistics for global temporary tables, accessible via
+    <link linkend="pg-gtr-info"><function>pg_gtr_info()</function></link> and the
+    <link linkend="pg-stat-activity-view"><structname>pg_stat_activity</structname></link>
+    view.  In particular, the <structfield>relfrozenxid</structfield> column
+    of a table's <structname>pg_class</structname> row (or the equivalent
+    in-memory value returned by <function>pg_gtr_info()</function>, for a
+    global temporary table) contains the oldest remaining unfrozen
     XID at the end of the most recent <command>VACUUM</command> that successfully
     advanced <structfield>relfrozenxid</structfield> (typically the most recent
-    aggressive VACUUM).  Similarly, the
+    aggressive VACUUM).  In addition, for each active backend, the
+    <structfield>tempfrozenxid</structfield> column of
+    <structname>pg_stat_activity</structname> contains the minimum
+    <structfield>relfrozenxid</structfield> over all global temporary tables
+    being used by that backend.  Similarly, the
     <structfield>datfrozenxid</structfield> column of a database's
     <structname>pg_database</structname> row is a lower bound on the unfrozen XIDs
-    appearing in that database &mdash; it is just the minimum of the
-    per-table <structfield>relfrozenxid</structfield> values within the database.
+    appearing in that database &mdash; it is the minimum of the per-table
+    <structfield>relfrozenxid</structfield> values within the database,
+    including all global temporary tables being used by backends connected
+    to the database.
     A convenient way to
     examine this information is to execute queries such as:
 
@@ -629,6 +640,10 @@ FROM pg_class c
 LEFT JOIN pg_class t ON c.reltoastrelid = t.oid
 WHERE c.relkind IN ('r', 'm');
 
+SELECT pid, age(tempfrozenxid)
+FROM pg_stat_activity
+WHERE datname = current_database();
+
 SELECT datname, age(datfrozenxid) FROM pg_database;
 </programlisting>
 
@@ -802,14 +817,18 @@ HINT:  Execute a database-wide VACUUM in that database.
      <xref linkend="guc-vacuum-multixact-freeze-min-age"/>
      by a different value, which can be the zero value, a single
      transaction ID, or a newer multixact ID.  For each table,
-     <structname>pg_class</structname>.<structfield>relminmxid</structfield> stores the oldest
+     <structname>pg_class</structname>.<structfield>relminmxid</structfield>
+     (or the equivalent in-memory value kept for a global temporary table,
+     which can be retrieved using <function>pg_gtr_info()</function>) is the oldest
      possible multixact ID still appearing in any tuple of that table.
      If this value is older than
      <xref linkend="guc-vacuum-multixact-freeze-table-age"/>, an aggressive
      vacuum is forced.  As discussed in the previous section, an aggressive
      vacuum means that only those pages which are known to be all-frozen will
      be skipped.  <function>mxid_age()</function> can be used on
-     <structname>pg_class</structname>.<structfield>relminmxid</structfield> to find its age.
+     <structname>pg_class</structname>.<structfield>relminmxid</structfield>
+     (or the equivalent value returned by <function>pg_gtr_info()</function>,
+     for a global tempoary table), to find its age.
     </para>
 
     <para>
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index f222cb54a30..8eef9489c45 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -1022,6 +1022,34 @@ postgres   27093  0.0  0.0  30096  2752 ?        Ss   11:34   0:00 postgres: ser
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>tempfrozenxid</structfield> <type>xid</type>
+      </para>
+      <para>
+       The minimum <literal>relfrozenxid</literal> over all global temporary
+       tables being used by this backend, or NULL if this backend hasn't used
+       any global temporary tables.  This can be used to determine which
+       backends need to vacuum or discard their global temporary tables, in
+       order to prevent transaction ID wraparound or to allow
+       <literal>pg_xact</literal> to be shrunk.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>tempminmxid</structfield> <type>xid</type>
+      </para>
+      <para>
+       The minimum <literal>relminmxid</literal> over all global temporary
+       tables being used by this backend, or NULL if this backend hasn't used
+       any global temporary tables.  This can be used to determine which
+       backends need to vacuum or discard their global temporary tables, in
+       order to prevent multixact ID wraparound or to allow
+       <literal>pg_multixact</literal> to be shrunk.
+      </para></entry>
+     </row>
+
     <row>
      <entry role="catalog_table_entry"><para role="column_definition">
       <structfield>query_id</structfield> <type>bigint</type>
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 8e1f660bc2f..9a4d8f32d33 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -918,7 +918,8 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
 								 PROGRESS_VACUUM_PHASE_FINAL_CLEANUP);
 
 	/*
-	 * Prepare to update rel's pg_class entry.
+	 * Prepare to update rel's pg_class entry and/or session-local GtrInfo
+	 * struct.
 	 *
 	 * Aggressive VACUUMs must always be able to advance relfrozenxid to a
 	 * value >= FreezeLimit, and relminmxid to a value >= MultiXactCutoff.
@@ -962,7 +963,8 @@ heap_vacuum_rel(Relation rel, const VacuumParams *params,
 		new_rel_allfrozen = new_rel_allvisible;
 
 	/*
-	 * Now actually update rel's pg_class entry.
+	 * Now actually update rel's pg_class entry and/or session-local GtrInfo
+	 * struct.
 	 *
 	 * In principle new_live_tuples could be -1 indicating that we (still)
 	 * don't know the tuple count.  In practice that can't happen, since we
diff --git a/src/backend/access/transam/twophase.c b/src/backend/access/transam/twophase.c
index 48e478a4ecb..eca85e9378f 100644
--- a/src/backend/access/transam/twophase.c
+++ b/src/backend/access/transam/twophase.c
@@ -78,6 +78,7 @@
 
 #include "access/commit_ts.h"
 #include "access/htup_details.h"
+#include "access/multixact.h"
 #include "access/subtrans.h"
 #include "access/transam.h"
 #include "access/twophase.h"
@@ -485,6 +486,8 @@ MarkAsPreparingGuts(GlobalTransaction gxact, FullTransactionId fxid,
 	/* subxid data must be filled later by GXactLoadSubxactData */
 	proc->subxidStatus.overflowed = false;
 	proc->subxidStatus.count = 0;
+	proc->tempfrozenxid = InvalidTransactionId;
+	proc->tempminmxid = InvalidMultiXactId;
 
 	gxact->prepared_at = prepared_at;
 	gxact->fxid = fxid;
diff --git a/src/backend/catalog/global_temp.c b/src/backend/catalog/global_temp.c
index 66d242ccabc..5c253bb5be6 100644
--- a/src/backend/catalog/global_temp.c
+++ b/src/backend/catalog/global_temp.c
@@ -53,6 +53,7 @@
 
 #include "access/amapi.h"
 #include "access/genam.h"
+#include "access/multixact.h"
 #include "access/parallel.h"
 #include "access/table.h"
 #include "access/tableam.h"
@@ -67,6 +68,7 @@
 #include "miscadmin.h"
 #include "storage/ipc.h"
 #include "storage/lwlock.h"
+#include "storage/proc.h"
 #include "storage/shmem.h"
 #include "storage/subsystems.h"
 #include "utils/fmgrprotos.h"
@@ -190,6 +192,14 @@ static List *gtrs_invalidated = NIL;
 static List *gtrs_dropped = NIL;
 static SubTransactionId processed_dropped_subid = InvalidSubTransactionId;
 
+/*
+ * update_tempfrozenxids
+ *
+ *	Flag indicating that this backend's tempfrozenxid and tempminmxid need to
+ *	be updated on commit.  See comments in UpdateTempFrozenXids().
+ */
+static bool update_tempfrozenxids = false;
+
 /*
  * gtr_shared_usage
  *
@@ -1065,6 +1075,17 @@ TrackGlobalTempRelation(Relation relation, bool isNew)
 
 		entry->history.subid = entry->started_subid;
 		entry->history.prev = NULL;
+
+		/*
+		 * If this backend's tempfrozenxid and tempminmxid haven't been set
+		 * yet (this is the first global temporary relation accessed in this
+		 * session), then update them to account for this relation.  If they
+		 * have been set, it must have been from an earlier transaction, so
+		 * this relation will not affect them.
+		 */
+		if (!TransactionIdIsValid(MyProc->tempfrozenxid) ||
+			!MultiXactIdIsValid(MyProc->tempminmxid))
+			UpdateTempFrozenXids();
 	}
 }
 
@@ -1089,6 +1110,9 @@ ForgetGlobalTempRelation(Oid relid)
 
 	entry->stopped_subid = GetCurrentSubTransactionId();
 	EOXactUsageListAdd(relid);
+
+	/* Update this backend's tempfrozenxid and tempminmxid */
+	UpdateTempFrozenXids();
 }
 
 /*
@@ -1257,11 +1281,34 @@ ProcessInvalidatedGlobalTempRelations(void)
 			remove_on_commit_action(relid);
 		}
 
+		/* Update this backend's tempfrozenxid and tempminmxid */
+		UpdateTempFrozenXids();
+
 		/* All dropped relations have been processed, as of this subxact */
 		processed_dropped_subid = GetCurrentSubTransactionId();
 	}
 }
 
+/*
+ * UpdateTempFrozenXids
+ *
+ *	Update this backend's tempfrozenxid and tempminmxid values, setting them
+ *	to the minimum relfrozenxid and relminmxid values over all in-use global
+ *	temporary tables.
+ *
+ *	Note: the updates are deferred until main transaction commit.  This is
+ *	necessary, in case some or all of the changes made in this transaction are
+ *	rolled back (e.g., a DROP that makes it seem as though tempfrozenxid
+ *	and/or tempminmxid can be advanced, only to be rolled back).  Other
+ *	backends must only see the final state that we commit.
+ */
+void
+UpdateTempFrozenXids(void)
+{
+	/* Flag tempfrozenxid and tempminmxid as to-be-updated on commit */
+	update_tempfrozenxids = true;
+}
+
 /*
  * AtEOXact_GlobalTempRelation
  *
@@ -1332,6 +1379,53 @@ AtEOXact_GlobalTempRelation(bool isCommit)
 		gtrs_dropped = NIL;
 	}
 	processed_dropped_subid = InvalidSubTransactionId;
+
+	/*
+	 * Finally, on commit, update tempfrozenxid and tempminmxid, if requested.
+	 *
+	 * Note that any usage records for dropped relations will have been
+	 * removed by this point.
+	 */
+	if (update_tempfrozenxids && isCommit)
+	{
+		TransactionId min_relfrozenxid;
+		MultiXactId min_relminmxid;
+
+		/* Scan all usage records for the new minimum xid values */
+		min_relfrozenxid = InvalidTransactionId;
+		min_relminmxid = InvalidMultiXactId;
+
+		hash_seq_init(&status, gtr_local_usage);
+		while ((usage_entry = hash_seq_search(&status)) != NULL)
+		{
+			TransactionId relfrozenxid;
+			MultiXactId relminmxid;
+
+			relfrozenxid = usage_entry->history.info.relfrozenxid;
+			relminmxid = usage_entry->history.info.relminmxid;
+
+			/* Ignore relations that don't hold unfrozen XIDs */
+			if (!TransactionIdIsValid(relfrozenxid) ||
+				!MultiXactIdIsValid(relminmxid))
+				continue;
+
+			/* Update the minimum xid values */
+			Assert(TransactionIdIsNormal(relfrozenxid));
+
+			if (!TransactionIdIsValid(min_relfrozenxid) ||
+				TransactionIdPrecedes(relfrozenxid, min_relfrozenxid))
+				min_relfrozenxid = relfrozenxid;
+
+			if (!MultiXactIdIsValid(min_relminmxid) ||
+				MultiXactIdPrecedes(relminmxid, min_relminmxid))
+				min_relminmxid = relminmxid;
+		}
+
+		/* Store the new values in our PGPROC struct */
+		MyProc->tempfrozenxid = min_relfrozenxid;
+		MyProc->tempminmxid = min_relminmxid;
+	}
+	update_tempfrozenxids = false;
 }
 
 /*
@@ -1667,8 +1761,8 @@ pg_gtr_info(PG_FUNCTION_ARGS)
 	Oid			relid = PG_GETARG_OID(0);
 	TupleDesc	tupdesc;
 	GtrInfo    *gtr_info;
-	Datum		values[6];
-	bool		nulls[6];
+	Datum		values[8];
+	bool		nulls[8];
 
 	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
 		elog(ERROR, "return type must be a row type");
@@ -1683,6 +1777,8 @@ pg_gtr_info(PG_FUNCTION_ARGS)
 	values[3] = Float4GetDatum(gtr_info->reltuples);
 	values[4] = Int32GetDatum(gtr_info->relallvisible);
 	values[5] = Int32GetDatum(gtr_info->relallfrozen);
+	values[6] = TransactionIdGetDatum(gtr_info->relfrozenxid);
+	values[7] = MultiXactIdGetDatum(gtr_info->relminmxid);
 
 	memset(nulls, 0, sizeof(nulls));
 
@@ -1707,7 +1803,7 @@ pg_gtrs_in_use(PG_FUNCTION_ARGS)
 
 	if (gtr_local_usage != NULL)
 	{
-		bool		nulls[7];
+		bool		nulls[9];
 		HASH_SEQ_STATUS status;
 		GtrUsageEntry *entry;
 
@@ -1719,7 +1815,7 @@ pg_gtrs_in_use(PG_FUNCTION_ARGS)
 		while ((entry = hash_seq_search(&status)) != NULL)
 		{
 			GtrInfo    *gtr_info = &entry->history.info;
-			Datum		values[7];
+			Datum		values[9];
 
 			/* Ignore dropped relations */
 			if (entry->stopped_subid != InvalidSubTransactionId)
@@ -1732,6 +1828,8 @@ pg_gtrs_in_use(PG_FUNCTION_ARGS)
 			values[4] = Float4GetDatum(gtr_info->reltuples);
 			values[5] = Int32GetDatum(gtr_info->relallvisible);
 			values[6] = Int32GetDatum(gtr_info->relallfrozen);
+			values[7] = TransactionIdGetDatum(gtr_info->relfrozenxid);
+			values[8] = MultiXactIdGetDatum(gtr_info->relminmxid);
 
 			tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
 								 values, nulls);
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 44412fe12b1..55dd497a116 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -980,8 +980,18 @@ InsertPgClassTuple(Relation pg_class_desc,
 	values[Anum_pg_class_relreplident - 1] = CharGetDatum(rd_rel->relreplident);
 	values[Anum_pg_class_relispartition - 1] = BoolGetDatum(rd_rel->relispartition);
 	values[Anum_pg_class_relrewrite - 1] = ObjectIdGetDatum(rd_rel->relrewrite);
-	values[Anum_pg_class_relfrozenxid - 1] = TransactionIdGetDatum(rd_rel->relfrozenxid);
-	values[Anum_pg_class_relminmxid - 1] = MultiXactIdGetDatum(rd_rel->relminmxid);
+
+	/*
+	 * For a global temporary relation, relfrozenxid and relminmxid are kept
+	 * in memory, in its session-local GtrInfo struct.  Set them to Invalid in
+	 * the pg_class tuple.
+	 */
+	values[Anum_pg_class_relfrozenxid - 1] =
+		TransactionIdGetDatum(rd_rel->relpersistence == RELPERSISTENCE_GLOBAL_TEMP ?
+							  InvalidTransactionId : rd_rel->relfrozenxid);
+	values[Anum_pg_class_relminmxid - 1] =
+		MultiXactIdGetDatum(rd_rel->relpersistence == RELPERSISTENCE_GLOBAL_TEMP ?
+							InvalidMultiXactId : rd_rel->relminmxid);
 	if (relacl != (Datum) 0)
 		values[Anum_pg_class_relacl - 1] = relacl;
 	else
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index ad340887f54..7b37a3a0b79 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -957,6 +957,8 @@ CREATE VIEW pg_stat_activity AS
             S.state,
             S.backend_xid,
             S.backend_xmin,
+            S.tempfrozenxid,
+            S.tempminmxid,
             S.query_id,
             S.query,
             S.backend_type
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 398049d1e98..66228ba31cc 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -260,6 +260,7 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	bool		verbose = false;
 	bool		analyze = false;
 	bool		concurrently = false;
+	bool		have_gtrs = false;
 
 	/* Parse option list */
 	foreach_node(DefElem, opt, stmt->params)
@@ -441,6 +442,9 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 			continue;
 		}
 
+		if (RELATION_IS_GLOBAL_TEMP(rel))
+			have_gtrs = true;
+
 		/* functions in indexes may want a snapshot set */
 		PushActiveSnapshot(GetTransactionSnapshot());
 
@@ -455,6 +459,13 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 	/* Start a new transaction for the cleanup work. */
 	StartTransactionCommand();
 
+	/*
+	 * Update this backend's tempfrozenxid and tempminmxid, if we processed
+	 * any global temporary relations.
+	 */
+	if (have_gtrs)
+		UpdateTempFrozenXids();
+
 	/* Clean up working storage */
 	MemoryContextDelete(repack_context);
 }
@@ -1835,8 +1846,8 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class,
 	{
 		Assert(!TransactionIdIsValid(frozenXid) ||
 			   TransactionIdIsNormal(frozenXid));
-		relform1->relfrozenxid = frozenXid;
-		relform1->relminmxid = cutoffMulti;
+		SetEffective_relfrozenxid(relform1, gtr_info1, frozenXid, NULL, NULL);
+		SetEffective_relminmxid(relform1, gtr_info1, cutoffMulti, NULL, NULL);
 	}
 
 	/* swap size statistics too, since new rel has freshly-updated stats */
@@ -2645,6 +2656,7 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 	else
 	{
 		Oid			indexOid = InvalidOid;
+		bool		is_gtr = RELATION_IS_GLOBAL_TEMP(rel);
 
 		indexOid = determine_clustered_index(rel, stmt->usingindex,
 											 stmt->indexname);
@@ -2677,6 +2689,13 @@ process_single_relation(RepackStmt *stmt, LOCKMODE lockmode, bool isTopLevel,
 			CommandCounterIncrement();
 		}
 
+		/*
+		 * Update this backend's tempfrozenxid and tempminmxid, if it was a
+		 * global temporary relation.
+		 */
+		if (is_gtr)
+			UpdateTempFrozenXids();
+
 		return NULL;
 	}
 }
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 758cf98b692..60696a3aaf0 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -130,7 +130,8 @@ static void vac_truncate_clog(TransactionId frozenXID,
 							  TransactionId lastSaneFrozenXid,
 							  MultiXactId lastSaneMinMulti);
 static bool vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
-					   BufferAccessStrategy bstrategy, bool isTopLevel);
+					   BufferAccessStrategy bstrategy, bool isTopLevel,
+					   bool *isGtr);
 static double compute_parallel_delay(void);
 static VacOptValue get_vacoptval_from_boolean(DefElem *def);
 static bool vac_tid_reaped(ItemPointer itemptr, void *state);
@@ -503,6 +504,7 @@ vacuum(List *relations, const VacuumParams *params, BufferAccessStrategy bstrate
 	const char *stmttype;
 	volatile bool in_outer_xact,
 				use_own_xacts;
+	bool		have_gtrs = false;
 
 	stmttype = (params->options & VACOPT_VACUUM) ? "VACUUM" : "ANALYZE";
 
@@ -631,11 +633,16 @@ vacuum(List *relations, const VacuumParams *params, BufferAccessStrategy bstrate
 		foreach(cur, relations)
 		{
 			VacuumRelation *vrel = lfirst_node(VacuumRelation, cur);
+			bool		isGtr = false;
+			bool		doAnalyse;
 
 			if (params->options & VACOPT_VACUUM)
 			{
-				if (!vacuum_rel(vrel->oid, vrel->relation, *params, bstrategy,
-								isTopLevel))
+				doAnalyse = vacuum_rel(vrel->oid, vrel->relation, *params,
+									   bstrategy, isTopLevel, &isGtr);
+				if (isGtr)
+					have_gtrs = true;
+				if (!doAnalyse)
 					continue;
 			}
 
@@ -703,6 +710,17 @@ vacuum(List *relations, const VacuumParams *params, BufferAccessStrategy bstrate
 		StartTransactionCommand();
 	}
 
+	if (params->options & VACOPT_VACUUM && !AmAutoVacuumWorkerProcess())
+	{
+		/*
+		 * Update this backend's tempfrozenxid and tempminmxid, if we vacuumed
+		 * any global temporary relations.  We skip this for autovacuum, which
+		 * cannot vacuum global temporary relations.
+		 */
+		if (have_gtrs)
+			UpdateTempFrozenXids();
+	}
+
 	if ((params->options & VACOPT_VACUUM) &&
 		!(params->options & VACOPT_SKIP_DATABASE_STATS))
 	{
@@ -1151,7 +1169,7 @@ vacuum_get_cutoffs(Relation rel, const VacuumParams *params,
 	freeze_table_age = params->freeze_table_age;
 	multixact_freeze_table_age = params->multixact_freeze_table_age;
 
-	/* Set pg_class fields in cutoffs */
+	/* Set pg_class / GtrInfo fields in cutoffs */
 	cutoffs->relfrozenxid = rel->rd_rel->relfrozenxid;
 	cutoffs->relminmxid = rel->rd_rel->relminmxid;
 
@@ -1551,8 +1569,15 @@ vac_update_relstats(Relation relation,
 	 * it's corrupt, and overwrite with the oldest remaining XID in the table.
 	 * This should match vac_update_datfrozenxid() concerning what we consider
 	 * to be "in the future".
-	 */
-	oldfrozenxid = pgcform->relfrozenxid;
+	 *
+	 * For a global temporary relation, frozenxid is only valid for the data
+	 * in our local instance of the relation, and is kept in memory, in its
+	 * session-local GtrInfo struct, instead of pg_class.  This contributes
+	 * towards tempfrozenxid for this backend and allows
+	 * vac_update_datfrozenxid() to advance datfrozenxid once every backend
+	 * accessing the relation has vacuumed it.
+	 */
+	oldfrozenxid = GetEffective_relfrozenxid(pgcform, gtr_info);
 	futurexid = false;
 	if (frozenxid_updated)
 		*frozenxid_updated = false;
@@ -1567,15 +1592,15 @@ vac_update_relstats(Relation relation,
 
 		if (update)
 		{
-			pgcform->relfrozenxid = frozenxid;
-			dirty = true;
+			SetEffective_relfrozenxid(pgcform, gtr_info, frozenxid,
+									  &dirty, &gtr_dirty);
 			if (frozenxid_updated)
 				*frozenxid_updated = true;
 		}
 	}
 
 	/* Similarly for relminmxid */
-	oldminmulti = pgcform->relminmxid;
+	oldminmulti = GetEffective_relminmxid(pgcform, gtr_info);
 	futuremxid = false;
 	if (minmulti_updated)
 		*minmulti_updated = false;
@@ -1590,8 +1615,8 @@ vac_update_relstats(Relation relation,
 
 		if (update)
 		{
-			pgcform->relminmxid = minmulti;
-			dirty = true;
+			SetEffective_relminmxid(pgcform, gtr_info, minmulti,
+									&dirty, &gtr_dirty);
 			if (minmulti_updated)
 				*minmulti_updated = true;
 		}
@@ -1632,6 +1657,52 @@ vac_update_relstats(Relation relation,
 }
 
 
+/*
+ *	vac_get_min_tempfrozenxids() -- get min temp XIDs over all backends
+ *
+ *		min_tempfrozenxid is set to the minimum tempfrozenxid from all
+ *		backends (including us) connected to our database.
+ *
+ *		min_tempminmxid is set to the minimum tempminmxid from all backends
+ *		(including us) connected to our database.
+ *
+ *		The values returned will be Invalid*Ids, if no backend is accessing
+ *		global temporary tables in our database.
+ */
+static void
+vac_get_min_tempfrozenxids(TransactionId *min_tempfrozenxid,
+						   MultiXactId *min_tempminmxid)
+{
+	/* Defaults, if no other backends found */
+	*min_tempfrozenxid = InvalidTransactionId;
+	*min_tempminmxid = InvalidMultiXactId;
+
+	LWLockAcquire(ProcArrayLock, LW_SHARED);
+	for (int i = 0; i < ProcGlobal->allProcCount; i++)
+	{
+		PGPROC	   *proc = GetPGProcByNumber(i);
+
+		/* Ignore backends not connected to our database */
+		if (proc->pid == 0)
+			continue;
+		if (proc->databaseId != MyDatabaseId)
+			continue;
+
+		/* Update the minimum return values */
+		if (TransactionIdIsValid(proc->tempfrozenxid) &&
+			(!TransactionIdIsValid(*min_tempfrozenxid) ||
+			 TransactionIdPrecedes(proc->tempfrozenxid, *min_tempfrozenxid)))
+			*min_tempfrozenxid = proc->tempfrozenxid;
+
+		if (MultiXactIdIsValid(proc->tempminmxid) &&
+			(!MultiXactIdIsValid(*min_tempminmxid) ||
+			 MultiXactIdPrecedes(proc->tempminmxid, *min_tempminmxid)))
+			*min_tempminmxid = proc->tempminmxid;
+	}
+	LWLockRelease(ProcArrayLock);
+}
+
+
 /*
  *	vac_update_datfrozenxid() -- update pg_database.datfrozenxid for our DB
  *
@@ -1666,6 +1737,8 @@ vac_update_datfrozenxid(void)
 	bool		dirty = false;
 	ScanKeyData key[1];
 	void	   *inplace_state;
+	TransactionId min_tempfrozenxid;
+	MultiXactId min_tempminmxid;
 
 	/*
 	 * Restrict this task to one backend per database.  This avoids race
@@ -1720,10 +1793,17 @@ vac_update_datfrozenxid(void)
 		/*
 		 * Only consider relations able to hold unfrozen XIDs (anything else
 		 * should have InvalidTransactionId in relfrozenxid anyway).
+		 *
+		 * We exclude global temporary relations here too because, although
+		 * they can hold unfrozen XIDs, their relfrozenxid and relminmxid
+		 * values are kept in memory, instead of in pg_class, and those values
+		 * contribute to tempfrozenxid and tempminmxid, which we account for
+		 * below.
 		 */
-		if (classForm->relkind != RELKIND_RELATION &&
-			classForm->relkind != RELKIND_MATVIEW &&
-			classForm->relkind != RELKIND_TOASTVALUE)
+		if ((classForm->relkind != RELKIND_RELATION &&
+			 classForm->relkind != RELKIND_MATVIEW &&
+			 classForm->relkind != RELKIND_TOASTVALUE) ||
+			classForm->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
 		{
 			Assert(!TransactionIdIsValid(relfrozenxid));
 			Assert(!MultiXactIdIsValid(relminmxid));
@@ -1780,6 +1860,31 @@ vac_update_datfrozenxid(void)
 	systable_endscan(scan);
 	table_close(relation, AccessShareLock);
 
+	/*
+	 * Account for tempfrozenxid and tempminmxid from all backends connected
+	 * to our database.  This amounts to min(gtr_info.relfrozenxid) and
+	 * min(gtr_info.relminmxid) over all those backends.
+	 */
+	vac_get_min_tempfrozenxids(&min_tempfrozenxid, &min_tempminmxid);
+
+	if (TransactionIdIsValid(min_tempfrozenxid))
+	{
+		Assert(TransactionIdIsNormal(min_tempfrozenxid));
+
+		if (TransactionIdPrecedes(lastSaneFrozenXid, min_tempfrozenxid))
+			bogus = true;
+		else if (TransactionIdPrecedes(min_tempfrozenxid, newFrozenXid))
+			newFrozenXid = min_tempfrozenxid;
+	}
+
+	if (MultiXactIdIsValid(min_tempminmxid))
+	{
+		if (MultiXactIdPrecedes(lastSaneMinMulti, min_tempminmxid))
+			bogus = true;
+		else if (MultiXactIdPrecedes(min_tempminmxid, newMinMulti))
+			newMinMulti = min_tempminmxid;
+	}
+
 	/* chicken out if bogus data found */
 	if (bogus)
 		return;
@@ -2050,7 +2155,7 @@ vac_truncate_clog(TransactionId frozenXID,
  */
 static bool
 vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
-		   BufferAccessStrategy bstrategy, bool isTopLevel)
+		   BufferAccessStrategy bstrategy, bool isTopLevel, bool *isGtr)
 {
 	LOCKMODE	lmode;
 	Relation	rel;
@@ -2138,6 +2243,10 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 		return false;
 	}
 
+	/* tell caller if it was a global temporary relation */
+	if (RELATION_IS_GLOBAL_TEMP(rel))
+		*isGtr = true;
+
 	/*
 	 * When recursing to a TOAST table, check privileges on the parent.  NB:
 	 * This is only safe to do because we hold a session lock on the main
@@ -2408,7 +2517,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams params,
 		toast_vacuum_params.toast_parent = relid;
 
 		vacuum_rel(toast_relid, NULL, toast_vacuum_params, bstrategy,
-				   isTopLevel);
+				   isTopLevel, isGtr);
 	}
 
 	/*
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index ab65a6dbcc9..c6b5f522efe 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -34,6 +34,7 @@
 #include <sys/time.h>
 
 #include "access/clog.h"
+#include "access/multixact.h"
 #include "access/transam.h"
 #include "access/twophase.h"
 #include "access/xlogutils.h"
@@ -523,6 +524,10 @@ InitProcess(void)
 	/* Initialize wait event information. */
 	MyProc->wait_event_info = 0;
 
+	/* Initialize global temporary table information */
+	MyProc->tempfrozenxid = InvalidTransactionId;
+	MyProc->tempminmxid = InvalidMultiXactId;
+
 	/* Initialize fields for group transaction status update. */
 	MyProc->clogGroupMember = false;
 	MyProc->clogGroupMemberXid = InvalidTransactionId;
@@ -689,6 +694,8 @@ InitAuxiliaryProcess(void)
 	MyProc->backendType = MyBackendType;
 	MyProc->delayChkptFlags = 0;
 	MyProc->statusFlags = 0;
+	MyProc->tempfrozenxid = InvalidTransactionId;
+	MyProc->tempminmxid = InvalidMultiXactId;
 	MyProc->lwWaiting = LW_WS_NOT_WAITING;
 	MyProc->lwWaitMode = 0;
 	MyProc->waitLock = NULL;
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 64b6f60516c..b4957075268 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -15,6 +15,7 @@
 #include "postgres.h"
 
 #include "access/htup_details.h"
+#include "access/multixact.h"
 #include "access/xlog.h"
 #include "access/xlogprefetcher.h"
 #include "catalog/catalog.h"
@@ -417,7 +418,7 @@ pg_stat_get_progress_info(PG_FUNCTION_ARGS)
 Datum
 pg_stat_get_activity(PG_FUNCTION_ARGS)
 {
-#define PG_STAT_GET_ACTIVITY_COLS	31
+#define PG_STAT_GET_ACTIVITY_COLS	33
 	int			num_backends = pgstat_fetch_stat_numbackends();
 	int			curr_backend;
 	int			pid = PG_ARGISNULL(0) ? -1 : PG_GETARG_INT32(0);
@@ -513,6 +514,12 @@ pg_stat_get_activity(PG_FUNCTION_ARGS)
 			/* leader_pid */
 			nulls[29] = true;
 
+			/* tempfrozenxid */
+			nulls[31] = true;
+
+			/* tempminmxid */
+			nulls[32] = true;
+
 			proc = BackendPidGetProc(beentry->st_procpid);
 
 			if (proc == NULL && (beentry->st_backendType != B_BACKEND))
@@ -525,19 +532,24 @@ pg_stat_get_activity(PG_FUNCTION_ARGS)
 			}
 
 			/*
-			 * If a PGPROC entry was retrieved, display wait events and lock
-			 * group leader or apply leader information if any.  To avoid
-			 * extra overhead, no extra lock is being held, so there is no
-			 * guarantee of consistency across multiple rows.
+			 * If a PGPROC entry was retrieved, display wait events, lock
+			 * group leader or apply leader information if any, tempfrozenxid
+			 * and tempminmxid.  To avoid extra overhead, no extra lock is
+			 * being held, so there is no guarantee of consistency across
+			 * multiple rows.
 			 */
 			if (proc != NULL)
 			{
 				uint32		raw_wait_event;
 				PGPROC	   *leader;
+				TransactionId tempfrozenxid;
+				MultiXactId tempminmxid;
 
 				raw_wait_event = UINT32_ACCESS_ONCE(proc->wait_event_info);
 				wait_event_type = pgstat_get_wait_event_type(raw_wait_event);
 				wait_event = pgstat_get_wait_event(raw_wait_event);
+				tempfrozenxid = (TransactionId) UINT32_ACCESS_ONCE(proc->tempfrozenxid);
+				tempminmxid = (MultiXactId) UINT32_ACCESS_ONCE(proc->tempminmxid);
 
 				leader = proc->lockGroupLeader;
 
@@ -561,6 +573,23 @@ pg_stat_get_activity(PG_FUNCTION_ARGS)
 						nulls[29] = false;
 					}
 				}
+
+				/*
+				 * Show tempfrozenxid and tempminmxid, if valid.  This leaves
+				 * these fields as NULL if these XIDs are invalid (0), which
+				 * indicates the backend is not using any global temporary
+				 * relations.
+				 */
+				if (TransactionIdIsValid(tempfrozenxid))
+				{
+					values[31] = TransactionIdGetDatum(tempfrozenxid);
+					nulls[31] = false;
+				}
+				if (MultiXactIdIsValid(tempminmxid))
+				{
+					values[32] = MultiXactIdGetDatum(tempminmxid);
+					nulls[32] = false;
+				}
 			}
 
 			if (wait_event_type)
@@ -760,6 +789,8 @@ pg_stat_get_activity(PG_FUNCTION_ARGS)
 			nulls[28] = true;
 			nulls[29] = true;
 			nulls[30] = true;
+			nulls[31] = true;
+			nulls[32] = true;
 		}
 
 		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index dbc139aac92..56636001b37 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -69,6 +69,7 @@
 #include "commands/policy.h"
 #include "commands/publicationcmds.h"
 #include "commands/trigger.h"
+#include "commands/vacuum.h"
 #include "common/int.h"
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
@@ -4027,7 +4028,7 @@ RelationSetNewRelfilenumber(Relation relation, char persistence)
 	}
 	else
 	{
-		/* Normal case, update the pg_class and GtrInfo */
+		/* Normal case, update the pg_class or GtrInfo (not both) */
 		SetEffective_relfilenode(classform, gtr_info, newrelfilenumber);
 
 		/* relpages etc. never change for sequences */
@@ -4039,11 +4040,25 @@ RelationSetNewRelfilenumber(Relation relation, char persistence)
 			SetEffective_relallvisible(classform, gtr_info, 0, NULL, NULL);
 			SetEffective_relallfrozen(classform, gtr_info, 0, NULL, NULL);
 		}
-		classform->relfrozenxid = freezeXid;
-		classform->relminmxid = minmulti;
-		classform->relpersistence = persistence;
+		SetEffective_relfrozenxid(classform, gtr_info, freezeXid, NULL, NULL);
+		SetEffective_relminmxid(classform, gtr_info, minmulti, NULL, NULL);
 
-		CatalogTupleUpdate(pg_class, &otid, tuple);
+		/* relpersistence can only change for permanent relations */
+		if (gtr_info != NULL)
+		{
+			Assert(classform->relpersistence == persistence);
+
+			/* No pg_class update, but we still need a relcache inval */
+			CacheInvalidateRelcacheByTuple(tuple);
+
+			/* Update this backend's tempfrozenxid and tempminmxid */
+			UpdateTempFrozenXids();
+		}
+		else
+		{
+			classform->relpersistence = persistence;
+			CatalogTupleUpdate(pg_class, &otid, tuple);
+		}
 	}
 
 	UnlockTuple(pg_class, &otid, InplaceUpdateTupleLock);
diff --git a/src/include/catalog/global_temp.h b/src/include/catalog/global_temp.h
index 584e219b24f..7030763fb51 100644
--- a/src/include/catalog/global_temp.h
+++ b/src/include/catalog/global_temp.h
@@ -33,6 +33,8 @@ typedef struct GtrInfo
 	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 */
+	TransactionId relfrozenxid; /* vac stats: all Xids < this are frozen */
+	MultiXactId relminmxid;		/* vac stats: all multixacts are >= this */
 
 	/* pg_index info */
 	bool		indisvalid;		/* is the index valid in this session? */
@@ -53,6 +55,8 @@ typedef struct GtrInfo
 		(target)->reltuples = (source)->reltuples; \
 		(target)->relallvisible = (source)->relallvisible; \
 		(target)->relallfrozen = (source)->relallfrozen; \
+		(target)->relfrozenxid = (source)->relfrozenxid; \
+		(target)->relminmxid = (source)->relminmxid; \
 	} while (0)
 
 extern void TrackGlobalTempRelationStorage(Oid relid, RelFileLocator rlocator,
@@ -65,6 +69,7 @@ extern void TrackGlobalTempRelation(Relation relation, bool isNew);
 extern void ForgetGlobalTempRelation(Oid relid);
 extern void InvalidateGlobalTempRelation(Oid relid);
 extern void ProcessInvalidatedGlobalTempRelations(void);
+extern void UpdateTempFrozenXids(void);
 extern void AtEOXact_GlobalTempRelation(bool isCommit);
 extern void AtEOSubXact_GlobalTempRelation(bool isCommit,
 										   SubTransactionId mySubid,
@@ -138,6 +143,26 @@ 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 relfrozenxid for a relation.  For a global
+ * temporary relation, the value from gtr_info (if present) takes precedence.
+ */
+static inline TransactionId
+GetEffective_relfrozenxid(Form_pg_class class_form, GtrInfo *gtr_info)
+{
+	return gtr_info != NULL ? gtr_info->relfrozenxid : class_form->relfrozenxid;
+}
+
+/*
+ * Get the effective value of relminmxid for a relation.  For a global
+ * temporary relation, the value from gtr_info (if present) takes precedence.
+ */
+static inline MultiXactId
+GetEffective_relminmxid(Form_pg_class class_form, GtrInfo *gtr_info)
+{
+	return gtr_info != NULL ? gtr_info->relminmxid : class_form->relminmxid;
+}
+
 /*
  * Get the effective value of indisvalid for an index relation.  For a global
  * temporary relation, the value from gtr_info (if present) takes precedence.
@@ -299,4 +324,64 @@ SetEffective_relallfrozen(Form_pg_class class_form, GtrInfo *gtr_info,
 	}
 }
 
+/*
+ * Set the effective value of relfrozenxid 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_relfrozenxid(Form_pg_class class_form, GtrInfo *gtr_info,
+						  TransactionId newval, bool *class_dirty, bool *gtr_dirty)
+{
+	if (gtr_info != NULL)
+	{
+		if (newval != gtr_info->relfrozenxid)
+		{
+			gtr_info->relfrozenxid = newval;
+			if (gtr_dirty != NULL)
+				*gtr_dirty = true;
+		}
+	}
+	else if (newval != class_form->relfrozenxid)
+	{
+		class_form->relfrozenxid = newval;
+		if (class_dirty != NULL)
+			*class_dirty = true;
+	}
+}
+
+/*
+ * Set the effective value of relminmxid 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_relminmxid(Form_pg_class class_form, GtrInfo *gtr_info,
+						MultiXactId newval, bool *class_dirty, bool *gtr_dirty)
+{
+	if (gtr_info != NULL)
+	{
+		if (newval != gtr_info->relminmxid)
+		{
+			gtr_info->relminmxid = newval;
+			if (gtr_dirty != NULL)
+				*gtr_dirty = true;
+		}
+	}
+	else if (newval != class_form->relminmxid)
+	{
+		class_form->relminmxid = 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 36668b728bd..cc8020b7f20 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5684,9 +5684,9 @@
   proname => 'pg_stat_get_activity', prorows => '100', proisstrict => 'f',
   proretset => 't', provolatile => 's', proparallel => 'r',
   prorettype => 'record', proargtypes => 'int4',
-  proallargtypes => '{int4,oid,int4,oid,text,text,text,text,text,timestamptz,timestamptz,timestamptz,timestamptz,inet,text,int4,xid,xid,text,bool,text,text,int4,text,numeric,text,bool,text,bool,bool,int4,int8}',
-  proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
-  proargnames => '{pid,datid,pid,usesysid,application_name,state,query,wait_event_type,wait_event,xact_start,query_start,backend_start,state_change,client_addr,client_hostname,client_port,backend_xid,backend_xmin,backend_type,ssl,sslversion,sslcipher,sslbits,ssl_client_dn,ssl_client_serial,ssl_issuer_dn,gss_auth,gss_princ,gss_enc,gss_delegation,leader_pid,query_id}',
+  proallargtypes => '{int4,oid,int4,oid,text,text,text,text,text,timestamptz,timestamptz,timestamptz,timestamptz,inet,text,int4,xid,xid,text,bool,text,text,int4,text,numeric,text,bool,text,bool,bool,int4,int8,xid,xid}',
+  proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{pid,datid,pid,usesysid,application_name,state,query,wait_event_type,wait_event,xact_start,query_start,backend_start,state_change,client_addr,client_hostname,client_port,backend_xid,backend_xmin,backend_type,ssl,sslversion,sslcipher,sslbits,ssl_client_dn,ssl_client_serial,ssl_issuer_dn,gss_auth,gss_princ,gss_enc,gss_delegation,leader_pid,query_id,tempfrozenxid,tempminmxid}',
   prosrc => 'pg_stat_get_activity' },
 { oid => '6318', descr => 'describe wait events',
   proname => 'pg_get_wait_events', procost => '10', prorows => '250',
@@ -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,int4,float4,int4,int4}',
-  proargmodes => '{i,o,o,o,o,o,o}',
-  proargnames => '{oid,relfilenode,reltablespace,relpages,reltuples,relallvisible,relallfrozen}',
+  proallargtypes => '{oid,oid,oid,int4,float4,int4,int4,xid,xid}',
+  proargmodes => '{i,o,o,o,o,o,o,o,o}',
+  proargnames => '{oid,relfilenode,reltablespace,relpages,reltuples,relallvisible,relallfrozen,relfrozenxid,relminmxid}',
   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,int4,float4,int4,int4}',
-  proargmodes => '{o,o,o,o,o,o,o}',
-  proargnames => '{oid,relfilenode,reltablespace,relpages,reltuples,relallvisible,relallfrozen}',
+  proallargtypes => '{oid,oid,oid,int4,float4,int4,int4,xid,xid}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o}',
+  proargnames => '{oid,relfilenode,reltablespace,relpages,reltuples,relallvisible,relallfrozen,relfrozenxid,relminmxid}',
   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/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 6e3c912bf5c..96bdfebaffe 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -265,7 +265,7 @@ typedef struct VacuumParams
 struct VacuumCutoffs
 {
 	/*
-	 * Existing pg_class fields at start of VACUUM
+	 * Existing pg_class / GtrInfo fields at start of VACUUM
 	 */
 	TransactionId relfrozenxid;
 	MultiXactId relminmxid;
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 4c3f431b4eb..473d8e478c2 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -383,6 +383,15 @@ typedef struct PGPROC
 	 ************************************************************************/
 
 	uint32		wait_event_info;	/* proc's wait information */
+
+	/************************************************************************
+	 * Global temporary tables
+	 ************************************************************************/
+
+	TransactionId tempfrozenxid;	/* minimum relfrozenxid over all global
+									 * temporary tables in use */
+	MultiXactId tempminmxid;	/* minimum relminmxid over all global
+								 * temporary tables in use */
 }
 PGPROC;
 
diff --git a/src/test/isolation/expected/vacuum-global-temp.out b/src/test/isolation/expected/vacuum-global-temp.out
new file mode 100644
index 00000000000..c8d9c375ff9
--- /dev/null
+++ b/src/test/isolation/expected/vacuum-global-temp.out
@@ -0,0 +1,127 @@
+Parsed test spec with 2 sessions
+
+starting permutation: create vacdml1 vac1 vacdml2 vac2 vacdml1 vac1prep vac1 vac1cmp vacdml2 vac2prep vac2 vac2cmp vacdml1 vac1prep vac1 vac1cmp vacdml2 vac2prep vac2 vac2cmp
+step create: 
+  CREATE GLOBAL TEMP TABLE vactest (a int);
+  CREATE TABLE saved_xids(local_xid bigint, global_xid bigint, db_xid bigint);
+
+  CREATE FUNCTION save_xids() RETURNS void
+  BEGIN ATOMIC
+    DELETE FROM saved_xids;
+    INSERT INTO saved_xids VALUES (
+      (SELECT min(t.relfrozenxid::text::bigint)
+         FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+        WHERE c.relpersistence = 'g' and t.relfrozenxid != 0),
+      (SELECT min(relfrozenxid::text::bigint) FROM pg_class WHERE relfrozenxid != 0),
+      (SELECT datfrozenxid::text::bigint FROM pg_database WHERE datname = current_database())
+    );
+  END;
+
+  CREATE FUNCTION cmp_xids(xid1 bigint, xid2 bigint) RETURNS text
+  BEGIN ATOMIC
+    SELECT CASE
+             WHEN xid1 < xid2 THEN 'older'
+             WHEN xid1 > xid2 THEN 'younger'
+             ELSE 'same'
+           END;
+  END;
+
+  CREATE FUNCTION check_new_xids(OUT local_xid text,
+                                 OUT global_xid text,
+                                 OUT db_xid text)
+  BEGIN ATOMIC
+    WITH new_xids(new_local_xid, new_global_xid, new_db_xid) AS (
+      SELECT
+        (SELECT min(t.relfrozenxid::text::bigint)
+           FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+          WHERE c.relpersistence = 'g' AND t.relfrozenxid != 0),
+        (SELECT min(relfrozenxid::text::bigint) FROM pg_class WHERE relfrozenxid != 0),
+        (SELECT datfrozenxid::text::bigint FROM pg_database WHERE datname = current_database())
+    )
+    SELECT cmp_xids(new_local_xid, local_xid),
+           cmp_xids(new_global_xid, global_xid),
+           cmp_xids(new_db_xid, db_xid)
+    FROM saved_xids, new_xids;
+  END;
+
+step vacdml1: 
+  INSERT INTO vactest SELECT * FROM generate_series(1, 10);
+  DELETE FROM vactest WHERE a % 2 = 0;
+  INSERT INTO vactest SELECT a * 2 FROM vactest;
+
+step vac1: VACUUM (FREEZE);
+step vacdml2: 
+  INSERT INTO vactest SELECT * FROM generate_series(1, 10);
+  UPDATE vactest SET a = a * 2 WHERE a % 2 = 1;
+
+step vac2: VACUUM (FREEZE);
+step vacdml1: 
+  INSERT INTO vactest SELECT * FROM generate_series(1, 10);
+  DELETE FROM vactest WHERE a % 2 = 0;
+  INSERT INTO vactest SELECT a * 2 FROM vactest;
+
+step vac1prep: SELECT save_xids();
+save_xids
+---------
+         
+(1 row)
+
+step vac1: VACUUM (FREEZE);
+step vac1cmp: SELECT * FROM check_new_xids();
+local_xid|global_xid|db_xid
+---------+----------+------
+younger  |younger   |same  
+(1 row)
+
+step vacdml2: 
+  INSERT INTO vactest SELECT * FROM generate_series(1, 10);
+  UPDATE vactest SET a = a * 2 WHERE a % 2 = 1;
+
+step vac2prep: SELECT save_xids();
+save_xids
+---------
+         
+(1 row)
+
+step vac2: VACUUM (FREEZE);
+step vac2cmp: SELECT * FROM check_new_xids();
+local_xid|global_xid|db_xid 
+---------+----------+-------
+younger  |younger   |younger
+(1 row)
+
+step vacdml1: 
+  INSERT INTO vactest SELECT * FROM generate_series(1, 10);
+  DELETE FROM vactest WHERE a % 2 = 0;
+  INSERT INTO vactest SELECT a * 2 FROM vactest;
+
+step vac1prep: SELECT save_xids();
+save_xids
+---------
+         
+(1 row)
+
+step vac1: VACUUM (FREEZE);
+step vac1cmp: SELECT * FROM check_new_xids();
+local_xid|global_xid|db_xid 
+---------+----------+-------
+younger  |younger   |younger
+(1 row)
+
+step vacdml2: 
+  INSERT INTO vactest SELECT * FROM generate_series(1, 10);
+  UPDATE vactest SET a = a * 2 WHERE a % 2 = 1;
+
+step vac2prep: SELECT save_xids();
+save_xids
+---------
+         
+(1 row)
+
+step vac2: VACUUM (FREEZE);
+step vac2cmp: SELECT * FROM check_new_xids();
+local_xid|global_xid|db_xid 
+---------+----------+-------
+younger  |younger   |younger
+(1 row)
+
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 2cf37a4caa4..7407673683e 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -132,3 +132,4 @@ test: tablespace-dependency-locking
 test: pub-concurrent-drop
 test: drop-owned-grant
 test: global-temp
+test: vacuum-global-temp
diff --git a/src/test/isolation/specs/vacuum-global-temp.spec b/src/test/isolation/specs/vacuum-global-temp.spec
new file mode 100644
index 00000000000..e3de3e773bd
--- /dev/null
+++ b/src/test/isolation/specs/vacuum-global-temp.spec
@@ -0,0 +1,75 @@
+# Test vacuuming global temporary relations
+
+teardown {
+  DROP FUNCTION save_xids, cmp_xids, check_new_xids;
+  DROP TABLE vactest, saved_xids;
+}
+
+session s1
+step create {
+  CREATE GLOBAL TEMP TABLE vactest (a int);
+  CREATE TABLE saved_xids(local_xid bigint, global_xid bigint, db_xid bigint);
+
+  CREATE FUNCTION save_xids() RETURNS void
+  BEGIN ATOMIC
+    DELETE FROM saved_xids;
+    INSERT INTO saved_xids VALUES (
+      (SELECT min(t.relfrozenxid::text::bigint)
+         FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+        WHERE c.relpersistence = 'g' and t.relfrozenxid != 0),
+      (SELECT min(relfrozenxid::text::bigint) FROM pg_class WHERE relfrozenxid != 0),
+      (SELECT datfrozenxid::text::bigint FROM pg_database WHERE datname = current_database())
+    );
+  END;
+
+  CREATE FUNCTION cmp_xids(xid1 bigint, xid2 bigint) RETURNS text
+  BEGIN ATOMIC
+    SELECT CASE
+             WHEN xid1 < xid2 THEN 'older'
+             WHEN xid1 > xid2 THEN 'younger'
+             ELSE 'same'
+           END;
+  END;
+
+  CREATE FUNCTION check_new_xids(OUT local_xid text,
+                                 OUT global_xid text,
+                                 OUT db_xid text)
+  BEGIN ATOMIC
+    WITH new_xids(new_local_xid, new_global_xid, new_db_xid) AS (
+      SELECT
+        (SELECT min(t.relfrozenxid::text::bigint)
+           FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+          WHERE c.relpersistence = 'g' AND t.relfrozenxid != 0),
+        (SELECT min(relfrozenxid::text::bigint) FROM pg_class WHERE relfrozenxid != 0),
+        (SELECT datfrozenxid::text::bigint FROM pg_database WHERE datname = current_database())
+    )
+    SELECT cmp_xids(new_local_xid, local_xid),
+           cmp_xids(new_global_xid, global_xid),
+           cmp_xids(new_db_xid, db_xid)
+    FROM saved_xids, new_xids;
+  END;
+}
+step vacdml1 {
+  INSERT INTO vactest SELECT * FROM generate_series(1, 10);
+  DELETE FROM vactest WHERE a % 2 = 0;
+  INSERT INTO vactest SELECT a * 2 FROM vactest;
+}
+step vac1prep { SELECT save_xids(); }
+step vac1 { VACUUM (FREEZE); }
+step vac1cmp { SELECT * FROM check_new_xids(); }
+
+session s2
+step vacdml2 {
+  INSERT INTO vactest SELECT * FROM generate_series(1, 10);
+  UPDATE vactest SET a = a * 2 WHERE a % 2 = 1;
+}
+step vac2prep { SELECT save_xids(); }
+step vac2 { VACUUM (FREEZE); }
+step vac2cmp { SELECT * FROM check_new_xids(); }
+
+permutation
+  create vacdml1 vac1 vacdml2 vac2
+  vacdml1 vac1prep vac1 vac1cmp
+  vacdml2 vac2prep vac2 vac2cmp
+  vacdml1 vac1prep vac1 vac1cmp
+  vacdml2 vac2prep vac2 vac2cmp
diff --git a/src/test/regress/expected/global_temp.out b/src/test/regress/expected/global_temp.out
index 88c80f4884c..a1538146f4a 100644
--- a/src/test/regress/expected/global_temp.out
+++ b/src/test/regress/expected/global_temp.out
@@ -48,6 +48,33 @@ ORDER BY table_name;
  regression    | global_temp_yyy   | tmp3       | GLOBAL TEMPORARY
 (3 rows)
 
+-- Check pg_stat_activity
+SELECT tempfrozenxid, tempminmxid -- not visible without privilege
+  FROM pg_stat_activity
+ WHERE pid = pg_backend_pid();
+ tempfrozenxid | tempminmxid 
+---------------+-------------
+               |            
+(1 row)
+
+RESET ROLE;
+GRANT pg_read_all_stats TO regress_global_temp_user;
+SET ROLE regress_global_temp_user;
+SELECT age(tempfrozenxid) - (SELECT max(age(t.relfrozenxid))
+                               FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+                              WHERE c.relpersistence = 'g'
+                                AND t.relfrozenxid != 0),
+       age(tempminmxid) - (SELECT max(age(t.relminmxid))
+                             FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+                            WHERE c.relpersistence = 'g'
+                              AND t.relminmxid != 0)
+  FROM pg_stat_activity
+ WHERE pid = pg_backend_pid();
+ ?column? | ?column? 
+----------+----------
+        0 |        0
+(1 row)
+
 DROP SCHEMA global_temp_xxx CASCADE;
 NOTICE:  drop cascades to table global_temp_xxx.tmp2
 DROP SCHEMA global_temp_yyy CASCADE;
@@ -71,14 +98,14 @@ SELECT * FROM tmp1;
 \c
 SET search_path = global_temp_tests;
 SELECT * FROM pg_gtr_info('tmp1'::regclass);
- relfilenode | reltablespace | relpages | reltuples | relallvisible | relallfrozen 
--------------+---------------+----------+-----------+---------------+--------------
-             |               |          |           |               |             
+ relfilenode | reltablespace | relpages | reltuples | relallvisible | relallfrozen | relfrozenxid | relminmxid 
+-------------+---------------+----------+-----------+---------------+--------------+--------------+------------
+             |               |          |           |               |              |              |           
 (1 row)
 
 SELECT * FROM pg_gtrs_in_use();
- oid | relfilenode | reltablespace | relpages | reltuples | relallvisible | relallfrozen 
------+-------------+---------------+----------+-----------+---------------+--------------
+ oid | relfilenode | reltablespace | relpages | reltuples | relallvisible | relallfrozen | relfrozenxid | relminmxid 
+-----+-------------+---------------+----------+-----------+---------------+--------------+--------------+------------
 (0 rows)
 
 SELECT * FROM tmp1;
@@ -93,12 +120,14 @@ SELECT c.relfilenode = c.oid,
        t.relpages = c.relpages,
        t.reltuples = c.reltuples,
        t.relallvisible = c.relallvisible,
-       t.relallfrozen = c.relallfrozen
+       t.relallfrozen = c.relallfrozen,
+       age(t.relfrozenxid) <= age(c.relfrozenxid),
+       age(t.relminmxid) <= age(c.relminmxid)
   FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
  WHERE c.oid = 'tmp1'::regclass;
- ?column? | ?column? | ?column? | ?column? | ?column? | ?column? | ?column? | ?column? 
-----------+----------+----------+----------+----------+----------+----------+----------
- t        | t        | t        | t        | t        | t        | t        | t
+ ?column? | ?column? | ?column? | ?column? | ?column? | ?column? | ?column? | ?column? | ?column? | ?column? 
+----------+----------+----------+----------+----------+----------+----------+----------+----------+----------
+ t        | t        | t        | t        | t        | t        | t        | t        | t        | t
 (1 row)
 
 SELECT c.relname,
@@ -107,13 +136,15 @@ SELECT c.relname,
        t.relpages = c.relpages,
        t.reltuples = c.reltuples,
        t.relallvisible = c.relallvisible,
-       t.relallfrozen = c.relallfrozen
+       t.relallfrozen = c.relallfrozen,
+       age(t.relfrozenxid) <= age(c.relfrozenxid),
+       age(t.relminmxid) <= age(c.relminmxid)
   FROM pg_gtrs_in_use() t LEFT JOIN pg_class c ON c.oid = t.oid
  ORDER BY c.relname;
-  relname  | ?column? | ?column? | ?column? | ?column? | ?column? | ?column? 
------------+----------+----------+----------+----------+----------+----------
- tmp1      | t        | t        | t        | t        | t        | t
- tmp1_pkey | t        | t        | t        | t        | t        | t
+  relname  | ?column? | ?column? | ?column? | ?column? | ?column? | ?column? | ?column? | ?column? 
+-----------+----------+----------+----------+----------+----------+----------+----------+----------
+ tmp1      | t        | t        | t        | t        | t        | t        | t        | t
+ tmp1_pkey | t        | t        | t        | t        | t        | t        | t        | t
 (2 rows)
 
 -- Test index
@@ -994,3 +1025,87 @@ SELECT oid::regclass, t.relpages, t.reltuples, t.relallvisible, t.relallfrozen
 (1 row)
 
 DROP TABLE tmp2;
+-- Test tempfrozenxid/tempminmxid update on commit
+\c
+SET search_path = global_temp_tests;
+SELECT tempfrozenxid, tempminmxid FROM pg_stat_activity WHERE pid = pg_backend_pid();
+ tempfrozenxid | tempminmxid 
+---------------+-------------
+               |            
+(1 row)
+
+BEGIN;
+CREATE GLOBAL TEMP TABLE tmp2 (a int);
+SELECT tempfrozenxid, tempminmxid FROM pg_stat_activity WHERE pid = pg_backend_pid();
+ tempfrozenxid | tempminmxid 
+---------------+-------------
+               |            
+(1 row)
+
+ROLLBACK;
+SELECT tempfrozenxid, tempminmxid FROM pg_stat_activity WHERE pid = pg_backend_pid();
+ tempfrozenxid | tempminmxid 
+---------------+-------------
+               |            
+(1 row)
+
+BEGIN;
+SAVEPOINT sp;
+CREATE GLOBAL TEMP TABLE tmp2 (a int);
+SELECT tempfrozenxid, tempminmxid FROM pg_stat_activity WHERE pid = pg_backend_pid();
+ tempfrozenxid | tempminmxid 
+---------------+-------------
+               |            
+(1 row)
+
+ROLLBACK TO sp;
+SELECT tempfrozenxid, tempminmxid FROM pg_stat_activity WHERE pid = pg_backend_pid();
+ tempfrozenxid | tempminmxid 
+---------------+-------------
+               |            
+(1 row)
+
+COMMIT;
+SELECT tempfrozenxid, tempminmxid FROM pg_stat_activity WHERE pid = pg_backend_pid();
+ tempfrozenxid | tempminmxid 
+---------------+-------------
+               |            
+(1 row)
+
+CREATE GLOBAL TEMP TABLE tmp2 (a int);
+SELECT age(tempfrozenxid) - (SELECT max(age(t.relfrozenxid))
+                               FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+                              WHERE c.relpersistence = 'g'
+                                AND t.relfrozenxid != 0),
+       age(tempminmxid) - (SELECT max(age(t.relminmxid))
+                             FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+                            WHERE c.relpersistence = 'g'
+                              AND t.relminmxid != 0)
+  FROM pg_stat_activity
+ WHERE pid = pg_backend_pid();
+ ?column? | ?column? 
+----------+----------
+        0 |        0
+(1 row)
+
+BEGIN;
+SAVEPOINT sp;
+DROP TABLE tmp2;
+ROLLBACK TO SAVEPOINT sp;
+COMMIT;
+SELECT age(tempfrozenxid) - (SELECT max(age(t.relfrozenxid))
+                               FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+                              WHERE c.relpersistence = 'g'
+                                AND t.relfrozenxid != 0),
+       age(tempminmxid) - (SELECT max(age(t.relminmxid))
+                             FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+                            WHERE c.relpersistence = 'g'
+                              AND t.relminmxid != 0)
+  FROM pg_stat_activity
+ WHERE pid = pg_backend_pid();
+ ?column? | ?column? 
+----------+----------
+        0 |        0
+(1 row)
+
+DROP TABLE tmp2;
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 4a8cc759d7b..db6dce62a29 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1795,10 +1795,12 @@ pg_stat_activity| SELECT s.datid,
     s.state,
     s.backend_xid,
     s.backend_xmin,
+    s.tempfrozenxid,
+    s.tempminmxid,
     s.query_id,
     s.query,
     s.backend_type
-   FROM ((pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id)
+   FROM ((pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id, tempfrozenxid, tempminmxid)
      LEFT JOIN pg_database d ON ((s.datid = d.oid)))
      LEFT JOIN pg_authid u ON ((s.usesysid = u.oid)));
 pg_stat_all_indexes| SELECT c.oid AS relid,
@@ -1944,7 +1946,7 @@ pg_stat_gssapi| SELECT pid,
     gss_princ AS principal,
     gss_enc AS encrypted,
     gss_delegation AS credentials_delegated
-   FROM pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id)
+   FROM pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id, tempfrozenxid, tempminmxid)
   WHERE (client_port IS NOT NULL);
 pg_stat_io| SELECT backend_type,
     object,
@@ -2259,7 +2261,7 @@ pg_stat_replication| SELECT s.pid,
     w.sync_priority,
     w.sync_state,
     w.reply_time
-   FROM ((pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id)
+   FROM ((pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id, tempfrozenxid, tempminmxid)
      JOIN pg_stat_get_wal_senders() w(pid, state, sent_lsn, write_lsn, flush_lsn, replay_lsn, write_lag, flush_lag, replay_lag, sync_priority, sync_state, reply_time) ON ((s.pid = w.pid)))
      LEFT JOIN pg_authid u ON ((s.usesysid = u.oid)));
 pg_stat_replication_slots| SELECT s.slot_name,
@@ -2296,7 +2298,7 @@ pg_stat_ssl| SELECT pid,
     ssl_client_dn AS client_dn,
     ssl_client_serial AS client_serial,
     ssl_issuer_dn AS issuer_dn
-   FROM pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id)
+   FROM pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, gss_delegation, leader_pid, query_id, tempfrozenxid, tempminmxid)
   WHERE (client_port IS NOT NULL);
 pg_stat_subscription| SELECT su.oid AS subid,
     su.subname,
diff --git a/src/test/regress/sql/global_temp.sql b/src/test/regress/sql/global_temp.sql
index 85b451d4cac..e0a15b66289 100644
--- a/src/test/regress/sql/global_temp.sql
+++ b/src/test/regress/sql/global_temp.sql
@@ -25,6 +25,26 @@ FROM information_schema.tables
 WHERE table_name ~ 'tmp' AND table_schema ~ 'global_temp'
 ORDER BY table_name;
 
+-- Check pg_stat_activity
+SELECT tempfrozenxid, tempminmxid -- not visible without privilege
+  FROM pg_stat_activity
+ WHERE pid = pg_backend_pid();
+
+RESET ROLE;
+GRANT pg_read_all_stats TO regress_global_temp_user;
+SET ROLE regress_global_temp_user;
+
+SELECT age(tempfrozenxid) - (SELECT max(age(t.relfrozenxid))
+                               FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+                              WHERE c.relpersistence = 'g'
+                                AND t.relfrozenxid != 0),
+       age(tempminmxid) - (SELECT max(age(t.relminmxid))
+                             FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+                            WHERE c.relpersistence = 'g'
+                              AND t.relminmxid != 0)
+  FROM pg_stat_activity
+ WHERE pid = pg_backend_pid();
+
 DROP SCHEMA global_temp_xxx CASCADE;
 DROP SCHEMA global_temp_yyy CASCADE;
 
@@ -50,7 +70,9 @@ SELECT c.relfilenode = c.oid,
        t.relpages = c.relpages,
        t.reltuples = c.reltuples,
        t.relallvisible = c.relallvisible,
-       t.relallfrozen = c.relallfrozen
+       t.relallfrozen = c.relallfrozen,
+       age(t.relfrozenxid) <= age(c.relfrozenxid),
+       age(t.relminmxid) <= age(c.relminmxid)
   FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
  WHERE c.oid = 'tmp1'::regclass;
 
@@ -60,7 +82,9 @@ SELECT c.relname,
        t.relpages = c.relpages,
        t.reltuples = c.reltuples,
        t.relallvisible = c.relallvisible,
-       t.relallfrozen = c.relallfrozen
+       t.relallfrozen = c.relallfrozen,
+       age(t.relfrozenxid) <= age(c.relfrozenxid),
+       age(t.relminmxid) <= age(c.relminmxid)
   FROM pg_gtrs_in_use() t LEFT JOIN pg_class c ON c.oid = t.oid
  ORDER BY c.relname;
 
@@ -520,3 +544,54 @@ SELECT oid::regclass, t.relpages, t.reltuples, t.relallvisible, t.relallfrozen
  WHERE c.oid = 'tmp2'::regclass;
 
 DROP TABLE tmp2;
+
+-- Test tempfrozenxid/tempminmxid update on commit
+\c
+SET search_path = global_temp_tests;
+SELECT tempfrozenxid, tempminmxid FROM pg_stat_activity WHERE pid = pg_backend_pid();
+
+BEGIN;
+CREATE GLOBAL TEMP TABLE tmp2 (a int);
+SELECT tempfrozenxid, tempminmxid FROM pg_stat_activity WHERE pid = pg_backend_pid();
+ROLLBACK;
+SELECT tempfrozenxid, tempminmxid FROM pg_stat_activity WHERE pid = pg_backend_pid();
+
+BEGIN;
+SAVEPOINT sp;
+CREATE GLOBAL TEMP TABLE tmp2 (a int);
+SELECT tempfrozenxid, tempminmxid FROM pg_stat_activity WHERE pid = pg_backend_pid();
+ROLLBACK TO sp;
+SELECT tempfrozenxid, tempminmxid FROM pg_stat_activity WHERE pid = pg_backend_pid();
+COMMIT;
+SELECT tempfrozenxid, tempminmxid FROM pg_stat_activity WHERE pid = pg_backend_pid();
+
+CREATE GLOBAL TEMP TABLE tmp2 (a int);
+SELECT age(tempfrozenxid) - (SELECT max(age(t.relfrozenxid))
+                               FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+                              WHERE c.relpersistence = 'g'
+                                AND t.relfrozenxid != 0),
+       age(tempminmxid) - (SELECT max(age(t.relminmxid))
+                             FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+                            WHERE c.relpersistence = 'g'
+                              AND t.relminmxid != 0)
+  FROM pg_stat_activity
+ WHERE pid = pg_backend_pid();
+
+BEGIN;
+SAVEPOINT sp;
+DROP TABLE tmp2;
+ROLLBACK TO SAVEPOINT sp;
+COMMIT;
+
+SELECT age(tempfrozenxid) - (SELECT max(age(t.relfrozenxid))
+                               FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+                              WHERE c.relpersistence = 'g'
+                                AND t.relfrozenxid != 0),
+       age(tempminmxid) - (SELECT max(age(t.relminmxid))
+                             FROM pg_class c, LATERAL pg_gtr_info(c.oid) t
+                            WHERE c.relpersistence = 'g'
+                              AND t.relminmxid != 0)
+  FROM pg_stat_activity
+ WHERE pid = pg_backend_pid();
+
+DROP TABLE tmp2;
-- 
2.51.0

