From ae03924a8419cf8b2d41b6c360bbe9f548aa87b3 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 v10 07/11] Add relfrozenxid and relminmxid columns to
 pg_temp_class.

This allows VACUUM results to be stored locally in pg_temp_class for
each global temporary table, while the corresponding pg_class fields
are kept invalid (zero).

Additionally, each session stores its minimum relfrozenxid and
relminmxid values from pg_temp_class 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                    |  47 ++++++
 doc/src/sgml/config.sgml                      |  18 ++-
 doc/src/sgml/maintenance.sgml                 |  19 ++-
 doc/src/sgml/monitoring.sgml                  |  28 ++++
 src/backend/access/heap/vacuumlazy.c          |   4 +-
 src/backend/access/transam/twophase.c         |   3 +
 src/backend/catalog/global_temp.c             |  63 ++++++++
 src/backend/catalog/heap.c                    |  13 +-
 src/backend/catalog/pg_temp_class.c           |   8 +
 src/backend/catalog/system_views.sql          |   2 +
 src/backend/commands/repack.c                 |  29 +++-
 src/backend/commands/vacuum.c                 | 138 ++++++++++++++++--
 src/backend/storage/lmgr/proc.c               |   7 +
 src/backend/utils/adt/pgstatfuncs.c           |  41 +++++-
 src/backend/utils/cache/gtcatcache.c          |  56 +++++++
 src/backend/utils/cache/relcache.c            |  21 ++-
 src/include/catalog/global_temp.h             |   1 +
 src/include/catalog/pg_proc.dat               |   6 +-
 src/include/catalog/pg_temp_class.h           |  82 +++++++++++
 src/include/commands/vacuum.h                 |   2 +-
 src/include/storage/proc.h                    |   9 ++
 src/include/utils/gtcatcache.h                |   2 +
 .../isolation/expected/vacuum-global-temp.out | 123 ++++++++++++++++
 src/test/isolation/isolation_schedule         |   1 +
 .../isolation/specs/vacuum-global-temp.spec   |  71 +++++++++
 src/test/regress/expected/global_temp.out     | 106 ++++++++++++++
 src/test/regress/expected/rules.out           |  10 +-
 src/test/regress/sql/global_temp.sql          |  66 +++++++++
 28 files changed, 923 insertions(+), 53 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 6b6789c3e1b..e616af12207 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -2362,6 +2362,11 @@ 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 value from
+       <link linkend="catalog-pg-temp-class"><structname>pg_temp_class</structname></link>.<structfield>relfrozenxid</structfield>
+       is used instead.
       </para></entry>
      </row>
 
@@ -2375,6 +2380,11 @@ 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 value from
+       <link linkend="catalog-pg-temp-class"><structname>pg_temp_class</structname></link>.<structfield>relminmxid</structfield>
+       is used instead.
       </para></entry>
      </row>
 
@@ -9194,6 +9204,43 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
        <link linkend="sql-createindex"><command>CREATE INDEX</command></link>.
       </para></entry>
      </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>relfrozenxid</structfield> <type>xid</type>
+      </para>
+      <para>
+       All transaction IDs before this one have been replaced with a permanent
+       (<quote>frozen</quote>) transaction ID in this table in the current
+       session.  This is used to track 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.
+       The minimum <structfield>relfrozenxid</structfield> over all global
+       temporary tables being used in each session is included in the
+       <literal>tempfrozenxid</literal> column of the
+       <link linkend="monitoring-pg-stat-activity-view"><structname>pg_stat_activity</structname></link>
+       view.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>relminmxid</structfield> <type>xid</type>
+      </para>
+      <para>
+       All multixact IDs before this one have been replaced by a
+       transaction ID in this table in the current session.  This is used to
+       track 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.  The minimum <structfield>relminmxid</structfield>
+       over all global temporary tables being used in each session is
+       included in the <literal>tempminmxid</literal> column of the
+       <link linkend="monitoring-pg-stat-activity-view"><structname>pg_stat_activity</structname></link>
+       view.
+      </para></entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 0165eb9ec02..20ecd79560e 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9655,7 +9655,8 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
        <listitem>
         <para>
          Specifies the maximum age (in transactions) that a table's
-         <structname>pg_class</structname>.<structfield>relfrozenxid</structfield> field can
+         <structname>pg_class</structname>.<structfield>relfrozenxid</structfield> or
+         <structname>pg_temp_class</structname>.<structfield>relfrozenxid</structfield> field can
          attain before a <command>VACUUM</command> operation is forced
          to prevent transaction ID wraparound within the table.
          Note that the system will launch autovacuum processes to
@@ -9684,7 +9685,8 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
        <listitem>
         <para>
          Specifies the maximum age (in multixacts) that a table's
-         <structname>pg_class</structname>.<structfield>relminmxid</structfield> field can
+         <structname>pg_class</structname>.<structfield>relminmxid</structfield> or
+         <structname>pg_temp_class</structname>.<structfield>relminmxid</structfield> field can
          attain before a <command>VACUUM</command> operation is forced to
          prevent multixact ID wraparound within the table.
          Note that the system will launch autovacuum processes to
@@ -10067,7 +10069,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> or
+         <structname>pg_temp_class</structname>.<structfield>relfrozenxid</structfield> field 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
@@ -10114,7 +10117,8 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
        <listitem>
         <para>
          Specifies the maximum age (in transactions) that a table's
-         <structname>pg_class</structname>.<structfield>relfrozenxid</structfield>
+         <structname>pg_class</structname>.<structfield>relfrozenxid</structfield> or
+         <structname>pg_temp_class</structname>.<structfield>relfrozenxid</structfield>
          field can attain before <command>VACUUM</command> takes
          extraordinary measures to avoid system-wide transaction ID
          wraparound failure.  This is <command>VACUUM</command>'s
@@ -10151,7 +10155,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> or
+         <structname>pg_temp_class</structname>.<structfield>relminmxid</structfield> field 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
@@ -10196,7 +10201,8 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
        <listitem>
         <para>
          Specifies the maximum age (in multixacts) that a table's
-         <structname>pg_class</structname>.<structfield>relminmxid</structfield>
+         <structname>pg_class</structname>.<structfield>relminmxid</structfield> or
+         <structname>pg_temp_class</structname>.<structfield>relminmxid</structfield>
          field can attain before <command>VACUUM</command> takes
          extraordinary measures to avoid system-wide multixact ID
          wraparound failure.  This is <command>VACUUM</command>'s
diff --git a/doc/src/sgml/maintenance.sgml b/doc/src/sgml/maintenance.sgml
index 84c5df9095b..1547755f36c 100644
--- a/doc/src/sgml/maintenance.sgml
+++ b/doc/src/sgml/maintenance.sgml
@@ -608,10 +608,13 @@
    <para>
     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
+    statistics in the system tables <structname>pg_class</structname>,
+    <structname>pg_temp_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_class</structname> row (or its
+    <structname>pg_temp_class</structname> row, 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
@@ -802,14 +805,16 @@ 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
+     <structname>pg_temp_class</structname>.<structfield>relminmxid</structfield> stores 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
+     <structname>pg_temp_class</structname>.<structfield>relminmxid</structfield> to find its age.
     </para>
 
     <para>
@@ -1112,7 +1117,8 @@ analyze threshold = analyze base threshold + analyze scale factor * number of tu
       <para>
        The <emphasis>transaction ID</emphasis> component measures the age in
        transactions of the table's
-       <structname>pg_class</structname>.<structfield>relfrozenxid</structfield>
+       <structname>pg_class</structname>.<structfield>relfrozenxid</structfield> or
+       <structname>pg_temp_class</structname>.<structfield>relfrozenxid</structfield>
        field as compared to <xref linkend="guc-autovacuum-freeze-max-age"/>.
        Furthermore, this component increases greatly once the age surpasses
        <xref linkend="guc-vacuum-failsafe-age"/>.  The final value for this
@@ -1128,7 +1134,8 @@ analyze threshold = analyze base threshold + analyze scale factor * number of tu
       <para>
        The <emphasis>multixact ID</emphasis> component measures the age in
        multixacts of the table's
-       <structname>pg_class</structname>.<structfield>relminmxid</structfield>
+       <structname>pg_class</structname>.<structfield>relminmxid</structfield> or
+       <structname>pg_temp_class</structname>.<structfield>relminmxid</structfield>
        field as compared to
        <xref linkend="guc-autovacuum-multixact-freeze-max-age"/>.  Furthermore,
        this component increases greatly once the age surpasses
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index e1e21bf89cb..352d76c7c75 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 063ef2208de..08de985f21c 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -918,7 +918,7 @@ 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 and/or pg_temp_class entries.
 	 *
 	 * Aggressive VACUUMs must always be able to advance relfrozenxid to a
 	 * value >= FreezeLimit, and relminmxid to a value >= MultiXactCutoff.
@@ -962,7 +962,7 @@ 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 and/or pg_temp_class entries.
 	 *
 	 * 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 133c775a080..a300b37ac37 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/gtcatcache.h"
@@ -168,6 +170,14 @@ static List *gtrs_dropped = NIL;
 static bool processing_invalidated_gtrs = false;
 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
  *
@@ -955,6 +965,17 @@ TrackGlobalTempRelation(Relation relation)
 	{
 		gtr_record_usage(relation->rd_id, relation->rd_rel->relkind);
 		InsertPgTempClassTuple(relation);
+
+		/*
+		 * 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();
 	}
 	Assert(PgTempClassTupleExists(relation->rd_id));
 }
@@ -983,6 +1004,9 @@ ForgetGlobalTempRelation(Oid relid)
 
 	/* Delete its pg_temp_class tuple */
 	DeletePgTempClassTuple(relid);
+
+	/* Update this backend's tempfrozenxid and tempminmxid */
+	UpdateTempFrozenXids();
 }
 
 /*
@@ -1173,6 +1197,9 @@ ProcessInvalidatedGlobalTempRelations(void)
 		if (tuples_deleted)
 			CommandCounterIncrement();
 
+		/* Update this backend's tempfrozenxid and tempminmxid */
+		UpdateTempFrozenXids();
+
 		/* All dropped relations have been processed, as of this subxact */
 		processed_dropped_subid = GetCurrentSubTransactionId();
 	}
@@ -1181,6 +1208,25 @@ ProcessInvalidatedGlobalTempRelations(void)
 	processing_invalidated_gtrs = false;
 }
 
+/*
+ * UpdateTempFrozenXids
+ *
+ *	Update this backend's tempfrozenxid and tempminmxid values, setting them
+ *	to the minimum relfrozenxid and relminmxid values from pg_temp_class.
+ *
+ *	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
  *
@@ -1258,6 +1304,23 @@ AtEOXact_GlobalTempRelation(bool isCommit)
 
 	/* Clean up global temporary catalog caches */
 	AtEOXact_GTCatCache(isCommit);
+
+	/*
+	 * Finally, on commit, update tempfrozenxid and tempminmxid, if requested.
+	 * This must be done after AtEOXact_GTCatCache(), so that it sees the
+	 * final state of pg_temp_class.
+	 */
+	if (update_tempfrozenxids && isCommit)
+	{
+		TransactionId min_relfrozenxid;
+		MultiXactId min_relminmxid;
+
+		GTCatCacheGetMinFrozenXids(&min_relfrozenxid, &min_relminmxid);
+
+		MyProc->tempfrozenxid = min_relfrozenxid;
+		MyProc->tempminmxid = min_relminmxid;
+	}
+	update_tempfrozenxids = false;
 }
 
 /*
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 5d32935d20e..97ea51382c3 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -979,8 +979,17 @@ 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 global temporary relations, relfrozenxid and relminmxid are stored
+	 * in pg_temp_class.  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/pg_temp_class.c b/src/backend/catalog/pg_temp_class.c
index af225541c1c..4d69b2b0422 100644
--- a/src/backend/catalog/pg_temp_class.c
+++ b/src/backend/catalog/pg_temp_class.c
@@ -74,6 +74,12 @@ get_pg_temp_class_tupdesc(void)
 		TupleDescInitEntry(tupdesc,
 						   (AttrNumber) Anum_pg_temp_class_relallfrozen,
 						   "relallfrozen", INT4OID, -1, 0);
+		TupleDescInitEntry(tupdesc,
+						   (AttrNumber) Anum_pg_temp_class_relfrozenxid,
+						   "relfrozenxid", XIDOID, -1, 0);
+		TupleDescInitEntry(tupdesc,
+						   (AttrNumber) Anum_pg_temp_class_relminmxid,
+						   "relminmxid", XIDOID, -1, 0);
 		TupleDescFinalize(tupdesc);
 
 		MemoryContextSwitchTo(oldcontext);
@@ -136,6 +142,8 @@ InsertPgTempClassTuple(Relation rel)
 	values[Anum_pg_temp_class_reltuples - 1] = Float4GetDatum(form->reltuples);
 	values[Anum_pg_temp_class_relallvisible - 1] = Int32GetDatum(form->relallvisible);
 	values[Anum_pg_temp_class_relallfrozen - 1] = Int32GetDatum(form->relallfrozen);
+	values[Anum_pg_temp_class_relfrozenxid - 1] = TransactionIdGetDatum(form->relfrozenxid);
+	values[Anum_pg_temp_class_relminmxid - 1] = MultiXactIdGetDatum(form->relminmxid);
 
 	GTCatCacheTupleInsert(PG_TEMP_CLASS,
 						  RelationGetRelid(rel),
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 8612d99a890..04e47a600b7 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 2deeb523848..3eb8a8c090a 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)
@@ -430,6 +431,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());
 
@@ -444,6 +448,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);
 }
@@ -1700,13 +1711,17 @@ swap_relation_files(Oid r1, Oid r2, bool target_is_pg_class,
 	 * and then fail to commit the pg_class update.
 	 */
 
-	/* set rel1's frozen Xid and minimum MultiXid */
+	/*
+	 * Set rel1's frozen Xid and minimum MultiXid.  For a global temporary
+	 * relation, the supplied values are set in pg_temp_class, instead of
+	 * pg_class.
+	 */
 	if (relform1->relkind != RELKIND_INDEX)
 	{
 		Assert(!TransactionIdIsValid(frozenXid) ||
 			   TransactionIdIsNormal(frozenXid));
-		relform1->relfrozenxid = frozenXid;
-		relform1->relminmxid = cutoffMulti;
+		SetEffective_relfrozenxid(relform1, temp_relform1, frozenXid, NULL, NULL);
+		SetEffective_relminmxid(relform1, temp_relform1, cutoffMulti, NULL, NULL);
 	}
 
 	/* swap size statistics too, since new rel has freshly-updated stats */
@@ -2560,6 +2575,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);
@@ -2592,6 +2608,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 1defddbf0e7..2bda20b1826 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 / pg_temp_class fields in cutoffs */
 	cutoffs->relfrozenxid = rel->rd_rel->relfrozenxid;
 	cutoffs->relminmxid = rel->rd_rel->relminmxid;
 
@@ -1567,8 +1585,14 @@ 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".
+	 *
+	 * For a global temporary relation, frozenxid is only valid for the data
+	 * in our local instance of the relation, and is stored in pg_temp_class,
+	 * 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 = pgcform->relfrozenxid;
+	oldfrozenxid = GetEffective_relfrozenxid(pgcform, temp_pgcform);
 	futurexid = false;
 	if (frozenxid_updated)
 		*frozenxid_updated = false;
@@ -1583,15 +1607,15 @@ vac_update_relstats(Relation relation,
 
 		if (update)
 		{
-			pgcform->relfrozenxid = frozenxid;
-			dirty = true;
+			SetEffective_relfrozenxid(pgcform, temp_pgcform, frozenxid,
+									  &dirty, &temp_dirty);
 			if (frozenxid_updated)
 				*frozenxid_updated = true;
 		}
 	}
 
 	/* Similarly for relminmxid */
-	oldminmulti = pgcform->relminmxid;
+	oldminmulti = GetEffective_relminmxid(pgcform, temp_pgcform);
 	futuremxid = false;
 	if (minmulti_updated)
 		*minmulti_updated = false;
@@ -1606,8 +1630,8 @@ vac_update_relstats(Relation relation,
 
 		if (update)
 		{
-			pgcform->relminmxid = minmulti;
-			dirty = true;
+			SetEffective_relminmxid(pgcform, temp_pgcform, minmulti,
+									&dirty, &temp_dirty);
 			if (minmulti_updated)
 				*minmulti_updated = true;
 		}
@@ -1646,6 +1670,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
  *
@@ -1680,6 +1750,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
@@ -1734,10 +1806,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 set in pg_temp_class, instead of 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));
@@ -1794,6 +1873,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(pg_temp_class.relfrozenxid) and
+	 * min(pg_temp_class.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;
@@ -2064,7 +2168,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;
@@ -2152,6 +2256,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
@@ -2439,7 +2547,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 624cecc6bc5..bb454930d47 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 0d47d745c18..da0461be996 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/gtcatcache.c b/src/backend/utils/cache/gtcatcache.c
index 82ca54c361d..371ae33faa3 100644
--- a/src/backend/utils/cache/gtcatcache.c
+++ b/src/backend/utils/cache/gtcatcache.c
@@ -825,6 +825,62 @@ GTCatCacheTupleDelete(GTCatCacheIdentifier cacheId, Oid relid)
 	}
 }
 
+/*
+ * GTCatCacheGetMinFrozenXids
+ *
+ *	Get the minimum relfrozenxid and relminmxid values from all pg_temp_class
+ *	cache entries.  If there are no pg_temp_class entries (no global temporary
+ *	relations have been used in this session), then Invalid*Ids are returned.
+ */
+void
+GTCatCacheGetMinFrozenXids(TransactionId *min_relfrozenxid,
+						   MultiXactId *min_relminmxid)
+{
+	GTCatCache *cache = &gt_cat_cache[PG_TEMP_CLASS];
+
+	/* Defaults, if no global temporary relations are being used */
+	*min_relfrozenxid = InvalidTransactionId;
+	*min_relminmxid = InvalidMultiXactId;
+
+	if (cache->hashtable != NULL)
+	{
+		HASH_SEQ_STATUS status;
+		GTCatCacheEntry *entry;
+
+		/* Scan all pg_temp_class entries and update the minimum xid values */
+		hash_seq_init(&status, cache->hashtable);
+		while ((entry = hash_seq_search(&status)) != NULL)
+		{
+			if (!entry->deleted)
+			{
+				Form_pg_temp_class temp_form;
+				TransactionId relfrozenxid;
+				MultiXactId relminmxid;
+
+				temp_form = (Form_pg_temp_class) GETSTRUCT(entry->tuple);
+				relfrozenxid = temp_form->relfrozenxid;
+				relminmxid = (MultiXactId) temp_form->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;
+			}
+		}
+	}
+}
+
 /*
  * GTCatCacheFlush
  *
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 868492d8f00..ea23e88e97e 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -70,6 +70,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"
@@ -4032,7 +4033,7 @@ RelationSetNewRelfilenumber(Relation relation, char persistence)
 	}
 	else
 	{
-		/* Normal case, update the pg_class and pg_temp_class entries */
+		/* Normal case, update pg_class or pg_temp_class entry (not both) */
 		SetEffective_relfilenode(classform, temp_classform, newrelfilenumber);
 
 		/* relpages etc. never change for sequences */
@@ -4044,15 +4045,23 @@ RelationSetNewRelfilenumber(Relation relation, char persistence)
 			SetEffective_relallvisible(classform, temp_classform, 0, NULL, NULL);
 			SetEffective_relallfrozen(classform, temp_classform, 0, NULL, NULL);
 		}
-		classform->relfrozenxid = freezeXid;
-		classform->relminmxid = minmulti;
-		classform->relpersistence = persistence;
+		SetEffective_relfrozenxid(classform, temp_classform, freezeXid, NULL, NULL);
+		SetEffective_relminmxid(classform, temp_classform, minmulti, NULL, NULL);
 
-		CatalogTupleUpdate(pg_class, &otid, tuple);
+		/* relpersistence can only change for permanent relations */
 		if (HeapTupleIsValid(temp_tuple))
 		{
+			Assert(classform->relpersistence == persistence);
 			UpdatePgTempClassTuple(RelationGetRelid(relation), temp_tuple);
 			heap_freetuple(temp_tuple);
+
+			/* Update this backend's tempfrozenxid and tempminmxid */
+			UpdateTempFrozenXids();
+		}
+		else
+		{
+			classform->relpersistence = persistence;
+			CatalogTupleUpdate(pg_class, &otid, tuple);
 		}
 	}
 
@@ -4062,7 +4071,7 @@ RelationSetNewRelfilenumber(Relation relation, char persistence)
 	table_close(pg_class, RowExclusiveLock);
 
 	/*
-	 * Make the pg_class and pg_temp_class row changes or relation map change
+	 * Make the pg_class/pg_temp_class row change or relation map change
 	 * visible.  This will cause the relcache entry to get updated, too.
 	 */
 	CommandCounterIncrement();
diff --git a/src/include/catalog/global_temp.h b/src/include/catalog/global_temp.h
index 9d33fa89fda..df30a069817 100644
--- a/src/include/catalog/global_temp.h
+++ b/src/include/catalog/global_temp.h
@@ -26,6 +26,7 @@ extern void TrackGlobalTempRelation(Relation relation);
 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,
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 6979c7d1161..854ec786ae7 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5687,9 +5687,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',
diff --git a/src/include/catalog/pg_temp_class.h b/src/include/catalog/pg_temp_class.h
index 55ba499ae5d..19d56e0a397 100644
--- a/src/include/catalog/pg_temp_class.h
+++ b/src/include/catalog/pg_temp_class.h
@@ -57,6 +57,12 @@ CATALOG(pg_temp_class,8082,TempRelationRelationId) BKI_TEMP_RELATION
 
 	/* # of all-frozen blocks (not always up-to-date) */
 	int32		relallfrozen BKI_DEFAULT(0);
+
+	/* all Xids < this are frozen in this rel */
+	TransactionId relfrozenxid BKI_DEFAULT(3);	/* FirstNormalTransactionId */
+
+	/* all multixacts in this rel are >= this; it is really a MultiXactId */
+	TransactionId relminmxid BKI_DEFAULT(1);	/* FirstMultiXactId */
 } FormData_pg_temp_class;
 
 END_CATALOG_STRUCT
@@ -87,6 +93,8 @@ MAKE_SYSCACHE(TEMPRELOID, pg_temp_class_oid_index, 128);
 		(target)->reltuples = (source)->reltuples; \
 		(target)->relallvisible = (source)->relallvisible; \
 		(target)->relallfrozen = (source)->relallfrozen; \
+		(target)->relfrozenxid = (source)->relfrozenxid; \
+		(target)->relminmxid = (source)->relminmxid; \
 	} while (0)
 
 /*
@@ -149,6 +157,26 @@ GetEffective_relallfrozen(Form_pg_class cf, Form_pg_temp_class tf)
 	return tf != NULL ? tf->relallfrozen : cf->relallfrozen;
 }
 
+/*
+ * Get the effective value of relfrozenxid from pg_class and pg_temp_class
+ * tuple data.  The value from pg_temp_class (if present) takes precedence.
+ */
+static inline TransactionId
+GetEffective_relfrozenxid(Form_pg_class cf, Form_pg_temp_class tf)
+{
+	return tf != NULL ? tf->relfrozenxid : cf->relfrozenxid;
+}
+
+/*
+ * Get the effective value of relminmxid from pg_class and pg_temp_class tuple
+ * data.  The value from pg_temp_class (if present) takes precedence.
+ */
+static inline MultiXactId
+GetEffective_relminmxid(Form_pg_class cf, Form_pg_temp_class tf)
+{
+	return tf != NULL ? tf->relminmxid : cf->relminmxid;
+}
+
 /*
  * Set the effective value of relfilenode in tuple form data from pg_class or
  * pg_temp_class.  The value is set in pg_temp_class instead of pg_class, if
@@ -285,6 +313,60 @@ SetEffective_relallfrozen(Form_pg_class cf, Form_pg_temp_class tf, int32 val,
 	}
 }
 
+/*
+ * Set the effective value of relfrozenxid in tuple form data from pg_class or
+ * pg_temp_class.  The value is set in pg_temp_class instead of pg_class, if
+ * the pg_temp_class tuple form data is non-NULL.  If non-NULL, the cdirty or
+ * tdirty flag is updated, if the value actually changes.
+ */
+static inline void
+SetEffective_relfrozenxid(Form_pg_class cf, Form_pg_temp_class tf,
+						  TransactionId val, bool *cdirty, bool *tdirty)
+{
+	if (tf != NULL)
+	{
+		if (val != tf->relfrozenxid)
+		{
+			tf->relfrozenxid = val;
+			if (tdirty != NULL)
+				*tdirty = true;
+		}
+	}
+	else if (val != cf->relfrozenxid)
+	{
+		cf->relfrozenxid = val;
+		if (cdirty != NULL)
+			*cdirty = true;
+	}
+}
+
+/*
+ * Set the effective value of relminmxid in tuple form data from pg_class or
+ * pg_temp_class.  The value is set in pg_temp_class instead of pg_class, if
+ * the pg_temp_class tuple form data is non-NULL.  If non-NULL, the cdirty or
+ * tdirty flag is updated, if the value actually changes.
+ */
+static inline void
+SetEffective_relminmxid(Form_pg_class cf, Form_pg_temp_class tf,
+						MultiXactId val, bool *cdirty, bool *tdirty)
+{
+	if (tf != NULL)
+	{
+		if (val != tf->relminmxid)
+		{
+			tf->relminmxid = val;
+			if (tdirty != NULL)
+				*tdirty = true;
+		}
+	}
+	else if (val != cf->relminmxid)
+	{
+		cf->relminmxid = val;
+		if (cdirty != NULL)
+			*cdirty = true;
+	}
+}
+
 extern bool PgTempClassTupleExists(Oid relid);
 extern HeapTuple GetPgTempClassTuple(Oid relid);
 extern void InsertPgTempClassTuple(Relation rel);
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 6e3c912bf5c..404d825d477 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 / pg_temp_class fields at start of VACUUM
 	 */
 	TransactionId relfrozenxid;
 	MultiXactId relminmxid;
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 03a1a466fa8..7d1cf3c8316 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -382,6 +382,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/include/utils/gtcatcache.h b/src/include/utils/gtcatcache.h
index ab57d4c5d4d..0879e37b469 100644
--- a/src/include/utils/gtcatcache.h
+++ b/src/include/utils/gtcatcache.h
@@ -35,6 +35,8 @@ extern void GTCatCacheTupleUpdate(GTCatCacheIdentifier cacheId, Oid relid,
 extern void GTCatCacheTupleUpdateInPlace(GTCatCacheIdentifier cacheId,
 										 Oid relid, HeapTuple newtuple);
 extern void GTCatCacheTupleDelete(GTCatCacheIdentifier cacheId, Oid relid);
+extern void GTCatCacheGetMinFrozenXids(TransactionId *min_relfrozenxid,
+									   MultiXactId *min_relminmxid);
 extern void GTCatCacheFlush(void);
 extern void AtEOXact_GTCatCache(bool isCommit);
 extern void AtEOSubXact_GTCatCache(bool isCommit, SubTransactionId mySubid,
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..0028fc9bfcc
--- /dev/null
+++ b/src/test/isolation/expected/vacuum-global-temp.out
@@ -0,0 +1,123 @@
+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(relfrozenxid::text::bigint) FROM pg_temp_class WHERE 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(relfrozenxid::text::bigint) FROM pg_temp_class WHERE 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 c26a0da5d59..a31c17189f8 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -131,3 +131,4 @@ test: ddl-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..8d765703aad
--- /dev/null
+++ b/src/test/isolation/specs/vacuum-global-temp.spec
@@ -0,0 +1,71 @@
+# 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(relfrozenxid::text::bigint) FROM pg_temp_class WHERE 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(relfrozenxid::text::bigint) FROM pg_temp_class WHERE 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 178cc681448..3cfc38443b3 100644
--- a/src/test/regress/expected/global_temp.out
+++ b/src/test/regress/expected/global_temp.out
@@ -48,6 +48,31 @@ 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 tempfrozenxid::text::bigint - (SELECT min(relfrozenxid::text::bigint)
+                                        FROM pg_temp_class
+                                       WHERE relfrozenxid::text != '0'),
+       tempminmxid::text::bigint - (SELECT min(relminmxid::text::bigint)
+                                      FROM pg_temp_class
+                                     WHERE relminmxid::text != '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;
@@ -871,3 +896,84 @@ SELECT nextval('s') FROM generate_series(1, 2);
      100
 (2 rows)
 
+-- 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 tempfrozenxid::text::bigint - (SELECT min(relfrozenxid::text::bigint)
+                                        FROM pg_temp_class
+                                       WHERE relfrozenxid::text != '0'),
+       tempminmxid::text::bigint - (SELECT min(relminmxid::text::bigint)
+                                      FROM pg_temp_class
+                                     WHERE relminmxid::text != '0')
+  FROM pg_stat_activity
+ WHERE pid = pg_backend_pid();
+ ?column? | ?column? 
+----------+----------
+        0 |        0
+(1 row)
+
+VACUUM FREEZE pg_temp_class;
+BEGIN;
+SAVEPOINT sp;
+DROP TABLE tmp2;
+ROLLBACK TO SAVEPOINT sp;
+COMMIT;
+SELECT tempfrozenxid::text::bigint - (SELECT min(relfrozenxid::text::bigint)
+                                        FROM pg_temp_class
+                                       WHERE relfrozenxid::text != '0'),
+       tempminmxid::text::bigint - (SELECT min(relminmxid::text::bigint)
+                                      FROM pg_temp_class
+                                     WHERE relminmxid::text != '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 1a29d46213e..e3469b34d4d 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,
@@ -2258,7 +2260,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,
@@ -2295,7 +2297,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 ef9902e5212..eae3f3d4698 100644
--- a/src/test/regress/sql/global_temp.sql
+++ b/src/test/regress/sql/global_temp.sql
@@ -25,6 +25,24 @@ 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 tempfrozenxid::text::bigint - (SELECT min(relfrozenxid::text::bigint)
+                                        FROM pg_temp_class
+                                       WHERE relfrozenxid::text != '0'),
+       tempminmxid::text::bigint - (SELECT min(relminmxid::text::bigint)
+                                      FROM pg_temp_class
+                                     WHERE relminmxid::text != '0')
+  FROM pg_stat_activity
+ WHERE pid = pg_backend_pid();
+
 DROP SCHEMA global_temp_xxx CASCADE;
 DROP SCHEMA global_temp_yyy CASCADE;
 
@@ -459,3 +477,51 @@ SELECT lastval();
 ROLLBACK;
 SELECT lastval();
 SELECT nextval('s') FROM generate_series(1, 2);
+
+-- 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 tempfrozenxid::text::bigint - (SELECT min(relfrozenxid::text::bigint)
+                                        FROM pg_temp_class
+                                       WHERE relfrozenxid::text != '0'),
+       tempminmxid::text::bigint - (SELECT min(relminmxid::text::bigint)
+                                      FROM pg_temp_class
+                                     WHERE relminmxid::text != '0')
+  FROM pg_stat_activity
+ WHERE pid = pg_backend_pid();
+
+VACUUM FREEZE pg_temp_class;
+BEGIN;
+SAVEPOINT sp;
+DROP TABLE tmp2;
+ROLLBACK TO SAVEPOINT sp;
+COMMIT;
+
+SELECT tempfrozenxid::text::bigint - (SELECT min(relfrozenxid::text::bigint)
+                                        FROM pg_temp_class
+                                       WHERE relfrozenxid::text != '0'),
+       tempminmxid::text::bigint - (SELECT min(relminmxid::text::bigint)
+                                      FROM pg_temp_class
+                                     WHERE relminmxid::text != '0')
+  FROM pg_stat_activity
+ WHERE pid = pg_backend_pid();
+
+DROP TABLE tmp2;
-- 
2.43.0

