From cb1b2f0536e4cad3273331f0ef7c006349ab33fd 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 v6 07/10] 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. Additionally, each session stores its
minimum pg_temp_class.relfrozenxid and pg_temp_class.relminmxid values
in its PGPROC struct, allowing other sessions to advance
pg_class.relfrozenxid and pg_class.relminmxid for global temporary
tables, taking into account the frozen XIDs from other sessions. Thus
it is possible to keep advancing datfrozenxid and datminmxid as long
as each session that uses global temporary tables runs VACUUM from
time to time.
---
 src/backend/access/heap/vacuumlazy.c          |   4 +-
 src/backend/access/transam/twophase.c         |   3 +
 src/backend/catalog/global_temp.c             |  17 +++
 src/backend/catalog/heap.c                    |  13 +-
 src/backend/catalog/pg_temp_class.c           | 129 ++++++++++++++++
 src/backend/commands/repack.c                 |  28 +++-
 src/backend/commands/vacuum.c                 | 138 ++++++++++++++++--
 src/backend/storage/lmgr/proc.c               |   7 +
 src/backend/utils/cache/relcache.c            |  78 +++++++---
 src/include/catalog/pg_temp_class.h           |  84 +++++++++++
 src/include/commands/vacuum.h                 |   2 +-
 src/include/storage/proc.h                    |   9 ++
 .../isolation/expected/vacuum-global-temp.out | 123 ++++++++++++++++
 src/test/isolation/isolation_schedule         |   1 +
 .../isolation/specs/vacuum-global-temp.spec   |  71 +++++++++
 15 files changed, 665 insertions(+), 42 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/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 39395aed0d5..cc77515ff58 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -919,7 +919,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.
@@ -963,7 +963,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 1035e8b3fc7..094ff7ed58e 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 b70fdc70f19..7a7e77a0d8e 100644
--- a/src/backend/catalog/global_temp.c
+++ b/src/backend/catalog/global_temp.c
@@ -55,6 +55,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"
@@ -69,6 +70,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/memutils.h"
@@ -836,11 +838,23 @@ InitGlobalTempRelation(Relation relation)
 	{
 		/* Create (and track) storage for the relation */
 		if (RELKIND_HAS_TABLE_AM(relation->rd_rel->relkind))
+		{
 			table_relation_set_new_filelocator(relation,
 											   &relation->rd_locator,
 											   relation->rd_rel->relpersistence,
 											   &relation->rd_rel->relfrozenxid,
 											   &relation->rd_rel->relminmxid);
+
+			/*
+			 * If tempfrozenxid and tempminmxid haven't been set for this
+			 * backend, then set them now (first global temporary table
+			 * accessed in this session).
+			 */
+			if (!TransactionIdIsValid(MyProc->tempfrozenxid))
+				MyProc->tempfrozenxid = relation->rd_rel->relfrozenxid;
+			if (!MultiXactIdIsValid(MyProc->tempminmxid))
+				MyProc->tempminmxid = (MultiXactId) relation->rd_rel->relminmxid;
+		}
 		else
 			RelationCreateStorage(relation->rd_id,
 								  relation->rd_locator,
@@ -1002,6 +1016,9 @@ GlobalTempRelationDropped(Oid relid)
 
 	/* Forget any ON COMMIT action for the relation */
 	remove_on_commit_action(relid);
+
+	/* Update this backend's tempfrozenxid and tempminmxid */
+	UpdateTempFrozenXids();
 }
 
 /*
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index fdd06ecc877..684e9482f15 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 3f21080d82c..c9d2a8f2943 100644
--- a/src/backend/catalog/pg_temp_class.c
+++ b/src/backend/catalog/pg_temp_class.c
@@ -52,11 +52,13 @@
 
 #include "access/genam.h"
 #include "access/htup_details.h"
+#include "access/multixact.h"
 #include "access/table.h"
 #include "access/xact.h"
 #include "catalog/indexing.h"
 #include "catalog/pg_temp_class.h"
 #include "miscadmin.h"
+#include "storage/proc.h"
 #include "utils/fmgroids.h"
 #include "utils/hsearch.h"
 #include "utils/memutils.h"
@@ -92,6 +94,14 @@ static MemoryContext pending_inserts_tupctx = NULL;
 /* Transaction nesting level the pending inserts are for */
 static int	pending_inserts_nest_level = 1;
 
+/*
+ * Latest minimum values of relfrozenxid and relminmxid over all global
+ * temporary tables, if changed in the current transaction.
+ */
+static TransactionId min_relfrozenxid = InvalidTransactionId;
+static MultiXactId min_relminmxid = InvalidMultiXactId;
+static bool min_frozenxids_updated = false;
+
 /*
  * init_pending_inserts_hashtable
  *
@@ -156,6 +166,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);
@@ -186,6 +202,8 @@ heap_form_pg_temp_class_tuple(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);
 
 	return heap_form_tuple(get_pg_temp_class_tupdesc(), values, nulls);
 }
@@ -582,6 +600,106 @@ GetEffectivePgClassTuple(Oid relid)
 	return tuple;
 }
 
+/*
+ * UpdateTempFrozenXids
+ *
+ *	Update the tempfrozenxid and tempminmxid values for this backend by
+ *	finding the minimum relfrozenxid and relminmxid values in pg_temp_class --
+ *	i.e., the minimum frozen XIDs over all global temporary relations in use
+ *	in this backend.
+ *
+ *	The new values are set in this process's PGPROC struct when the current
+ *	transaction is committed, or discarded if the transaction is rolled back.
+ *
+ *	If no global temporary relations are in use, Invalid*Ids will be set.
+ */
+void
+UpdateTempFrozenXids(void)
+{
+	HASH_SEQ_STATUS status;
+	PendingInsert *entry;
+	Form_pg_temp_class temp_form;
+	TransactionId relfrozenxid;
+	MultiXactId relminmxid;
+	Relation	pg_temp_class;
+	SysScanDesc scan;
+	HeapTuple	tuple;
+
+	/* Defaults, if no global temporary relations are being used */
+	min_relfrozenxid = InvalidTransactionId;
+	min_relminmxid = InvalidMultiXactId;
+
+	/* Processing any pending inserts */
+	if (have_pending_inserts)
+	{
+		hash_seq_init(&status, pending_inserts);
+		while ((entry = hash_seq_search(&status)) != NULL)
+		{
+			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 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;
+		}
+	}
+
+	/*
+	 * Process all pg_temp_class entries.  If we haven't opened pg_temp_class
+	 * in this session yet, it must be empty, and we can skip it.
+	 */
+	if (pg_temp_class_opened)
+	{
+		pg_temp_class = table_open(TempRelationRelationId, AccessShareLock);
+
+		scan = systable_beginscan(pg_temp_class, InvalidOid, false,
+								  NULL, 0, NULL);
+
+		while ((tuple = systable_getnext(scan)) != NULL)
+		{
+			temp_form = (Form_pg_temp_class) GETSTRUCT(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 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;
+		}
+
+		/* Tidy up */
+		systable_endscan(scan);
+		table_close(pg_temp_class, AccessShareLock);
+	}
+
+	/* Flag the new values as to be applied on commit */
+	min_frozenxids_updated = true;
+}
+
 /*
  * PreCCI_PgTempClass
  *
@@ -644,6 +762,17 @@ AtEOXact_PgTempClass(bool isCommit)
 	 * transaction.
 	 */
 	pending_inserts_nest_level = 1;
+
+	/*
+	 * On commit, save any new tempfrozenxid and tempminmxid values to our
+	 * PGPROC struct.  On rollback, any new values are simply discarded.
+	 */
+	if (min_frozenxids_updated && isCommit)
+	{
+		MyProc->tempfrozenxid = min_relfrozenxid;
+		MyProc->tempminmxid = min_relminmxid;
+	}
+	min_frozenxids_updated = false;
 }
 
 /*
diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c
index 5f11e699dc4..0c704a531b2 100644
--- a/src/backend/commands/repack.c
+++ b/src/backend/commands/repack.c
@@ -259,6 +259,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)
@@ -463,6 +464,8 @@ ExecRepack(ParseState *pstate, RepackStmt *stmt, bool isTopLevel)
 			CommitTransactionCommand();
 			continue;
 		}
+		if (RELATION_IS_GLOBAL_TEMP(rel))
+			have_gtrs = true;
 
 		/* functions in indexes may want a snapshot set */
 		PushActiveSnapshot(GetTransactionSnapshot());
@@ -478,6 +481,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);
 }
@@ -1728,13 +1738,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 */
@@ -2522,6 +2536,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);
@@ -2554,6 +2569,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 9c776d5a978..6ae3fa0d6a6 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -128,7 +128,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);
@@ -500,6 +501,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";
 
@@ -628,11 +630,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;
 			}
 
@@ -700,6 +707,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
+		 * shouldn't vacuum any global temporary relations.
+		 */
+		if (have_gtrs)
+			UpdateTempFrozenXids();
+	}
+
 	if ((params->options & VACOPT_VACUUM) &&
 		!(params->options & VACOPT_SKIP_DATABASE_STATS))
 	{
@@ -1125,7 +1143,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;
 
@@ -1543,8 +1561,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;
@@ -1559,15 +1583,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;
@@ -1582,8 +1606,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;
 		}
@@ -1622,6 +1646,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
  *
@@ -1656,6 +1726,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
@@ -1710,10 +1782,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));
@@ -1770,6 +1849,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;
@@ -2040,7 +2144,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;
@@ -2126,6 +2230,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
@@ -2392,7 +2500,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 7d01c981a1f..a9205e449d1 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;
@@ -686,6 +691,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/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 274132ced87..e857e36eb4a 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"
@@ -303,6 +304,7 @@ static void formrdesc(const char *relationName, Oid relationReltype,
 					  bool isshared, int natts, const FormData_pg_attribute *attrs);
 
 static HeapTuple ScanPgRelation(Oid targetRelId, bool indexOK, bool force_non_historic);
+static void ScanPgTempRelation(Oid relid, Form_pg_class pg_class_form);
 static Relation AllocateRelationDesc(Form_pg_class relp);
 static void RelationParseRelOptions(Relation relation, HeapTuple tuple);
 static void RelationBuildTupleDesc(Relation relation);
@@ -415,7 +417,8 @@ ScanPgRelation(Oid targetRelId, bool indexOK, bool force_non_historic)
 	 * this for pg_temp_class itself, or its index, because they may not have
 	 * been loaded yet.  That's OK because we only really need relfilenumber
 	 * and reltablespace to be correct at this stage, and we disallow changes
-	 * to those attributes for these relations.
+	 * to those attributes for these relations.  Final initialization of
+	 * pg_temp_class and its index is performed by RelationIdGetRelation().
 	 */
 	if (HeapTupleIsValid(pg_class_tuple) &&
 		targetRelId != TempRelationRelationId &&
@@ -426,23 +429,40 @@ ScanPgRelation(Oid targetRelId, bool indexOK, bool force_non_historic)
 		pg_class_form = (Form_pg_class) GETSTRUCT(pg_class_tuple);
 
 		if (pg_class_form->relpersistence == RELPERSISTENCE_GLOBAL_TEMP)
-		{
-			HeapTuple	pg_temp_class_tuple;
-			Form_pg_temp_class pg_temp_class_form;
-
-			pg_temp_class_tuple = GetPgTempClassTuple(targetRelId);
-			if (HeapTupleIsValid(pg_temp_class_tuple))
-			{
-				pg_temp_class_form = (Form_pg_temp_class) GETSTRUCT(pg_temp_class_tuple);
-				COPY_PG_TEMP_CLASS_ATTRS(pg_temp_class_form, pg_class_form);
-				heap_freetuple(pg_temp_class_tuple);
-			}
-		}
+			ScanPgTempRelation(targetRelId, pg_class_form);
 	}
 
 	return pg_class_tuple;
 }
 
+/*
+ *		ScanPgTempRelation
+ *
+ *		This is used by ScanPgRelation to update a global temporary relation's
+ *		relcache entry from its pg_temp_class tuple.
+ *
+ *		In addition, it is used by RelationIdGetRelation() to perform final
+ *		intialization for pg_temp_class itself, and its index --- as noted
+ *		above, ScanPgRelation() cannot scan pg_temp_class when initializing
+ *		pg_temp_class itself, or its index, and so this is used after building
+ *		initial relcache entries, creating storage, and inserting
+ *		pg_temp_class tuples for these relations.
+ */
+static void
+ScanPgTempRelation(Oid relid, Form_pg_class pg_class_form)
+{
+	HeapTuple	pg_temp_class_tuple;
+	Form_pg_temp_class pg_temp_class_form;
+
+	pg_temp_class_tuple = GetPgTempClassTuple(relid);
+	if (HeapTupleIsValid(pg_temp_class_tuple))
+	{
+		pg_temp_class_form = (Form_pg_temp_class) GETSTRUCT(pg_temp_class_tuple);
+		COPY_PG_TEMP_CLASS_ATTRS(pg_temp_class_form, pg_class_form);
+		heap_freetuple(pg_temp_class_tuple);
+	}
+}
+
 /*
  *		AllocateRelationDesc
  *
@@ -2164,6 +2184,15 @@ RelationIdGetRelation(Oid relationId)
 			if (RELATION_IS_GLOBAL_TEMP(rd))
 				InitGlobalTempRelation(rd);
 
+			/*
+			 * Special-case: if it's pg_temp_class or its index, update its
+			 * relcache entry from the pg_temp_class tuple (ScanPgRelation()
+			 * couldn't do this when first loading these relations).
+			 */
+			if (rd->rd_id == TempRelationRelationId ||
+				rd->rd_id == TempClassOidIndexId)
+				ScanPgTempRelation(rd->rd_id, rd->rd_rel);
+
 			/*
 			 * Normally entries need to be valid here, but before the relcache
 			 * has been initialized, not enough infrastructure exists to
@@ -2187,6 +2216,9 @@ RelationIdGetRelation(Oid relationId)
 		RelationIncrementReferenceCount(rd);
 		if (RELATION_IS_GLOBAL_TEMP(rd))
 			InitGlobalTempRelation(rd);
+		if (rd->rd_id == TempRelationRelationId ||
+			rd->rd_id == TempClassOidIndexId)
+			ScanPgTempRelation(rd->rd_id, rd->rd_rel);
 	}
 	return rd;
 }
@@ -4034,7 +4066,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 */
@@ -4046,15 +4078,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);
 		}
 	}
 
@@ -4064,7 +4104,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/pg_temp_class.h b/src/include/catalog/pg_temp_class.h
index e6b77b50d23..a4629b12c9d 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 HeapTuple GetPgTempClassTuple(Oid relid);
 
@@ -302,6 +384,8 @@ extern HeapTuple GetPgClassAndPgTempClassTuples(Oid relid, bool lock_tuple,
 
 extern HeapTuple GetEffectivePgClassTuple(Oid relid);
 
+extern void UpdateTempFrozenXids(void);
+
 extern void PreCCI_PgTempClass(void);
 
 extern void PreCommit_PgTempClass(void);
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 956d9cea36d..9bd5e1ef717 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -257,7 +257,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 3e1d1fad5f9..710b466f173 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/test/isolation/expected/vacuum-global-temp.out b/src/test/isolation/expected/vacuum-global-temp.out
new file mode 100644
index 00000000000..6c8d62a672c
--- /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 int, global_xid int, db_xid int);
+
+  CREATE FUNCTION save_xids() RETURNS void
+  BEGIN ATOMIC
+    DELETE FROM saved_xids;
+    INSERT INTO saved_xids VALUES (
+      (SELECT min(relfrozenxid::text::int) FROM pg_temp_class WHERE relfrozenxid != 0),
+      (SELECT min(relfrozenxid::text::int) FROM pg_class WHERE relfrozenxid != 0),
+      (SELECT datfrozenxid::text::int FROM pg_database WHERE datname = current_database())
+    );
+  END;
+
+  CREATE FUNCTION cmp_xids(xid1 int, xid2 int) 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::int) FROM pg_temp_class WHERE relfrozenxid != 0),
+        (SELECT min(relfrozenxid::text::int) FROM pg_class WHERE relfrozenxid != 0),
+        (SELECT datfrozenxid::text::int 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 7d1aacec267..4253d60d134 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -129,3 +129,4 @@ test: lock-nowait
 test: for-portion-of
 test: ddl-dependency-locking
 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..4ab5b5e3b8c
--- /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 int, global_xid int, db_xid int);
+
+  CREATE FUNCTION save_xids() RETURNS void
+  BEGIN ATOMIC
+    DELETE FROM saved_xids;
+    INSERT INTO saved_xids VALUES (
+      (SELECT min(relfrozenxid::text::int) FROM pg_temp_class WHERE relfrozenxid != 0),
+      (SELECT min(relfrozenxid::text::int) FROM pg_class WHERE relfrozenxid != 0),
+      (SELECT datfrozenxid::text::int FROM pg_database WHERE datname = current_database())
+    );
+  END;
+
+  CREATE FUNCTION cmp_xids(xid1 int, xid2 int) 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::int) FROM pg_temp_class WHERE relfrozenxid != 0),
+        (SELECT min(relfrozenxid::text::int) FROM pg_class WHERE relfrozenxid != 0),
+        (SELECT datfrozenxid::text::int 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
-- 
2.51.0

