From 1ff3564ecb457ac17332077a6135a505ea50d5d2 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Mon, 5 Sep 2022 17:46:34 -0700
Subject: [PATCH v8 5/6] Make VACUUM's aggressive behaviors continuous.

The concept of aggressive/scan_all VACUUM dates back to the introduction
of the visibility map in Postgres 8.4.  Before then, every lazy VACUUM
was "equally aggressive": each operation froze whatever tuples before
the age-wise cutoff needed to be frozen.  And each table's relfrozenxid
was updated at the end.  In short, the previous behavior was much less
efficient, but did at least have one thing going for it: it was much
easier to understand at a high level.

VACUUM no longer applies a separate mode of operation (aggressive mode).
There are still antiwraparound autovacuums, but they're now little more
than another way that autovacuum.c can launch an autovacuum worker to
run VACUUM.  The same set of behaviors previously associated with
aggressive mode are retained, but now get applied selectively, on a
timeline attuned to the needs of the table.

The closer that a table's age gets to the autovacuum_freeze_max_age
cutoff, the less VACUUM will care about avoiding the cost of scanning
extra pages to advance relfrozenxid "early".  This new approach cares
about both costs (extra pages scanned) and benefits (the need for
relfrozenxid advancement), unlike the previous approach driven by
vacuum_freeze_table_age, which "escalated to aggressive mode" purely
based on a simple XID age cutoff.  The vacuum_freeze_table_age GUC is
now relegated to a compatibility option.  Its default value is now -1,
which is interpreted as "current value of autovacuum_freeze_max_age".

VACUUM will still advance relfrozenxid at roughly the same XID-age-wise
cadence as before with static tables, but can also advance relfrozenxid
much more frequently in tables where that happens to make sense.  In
practice many tables will tend to have relfrozenxid advanced by some
amount during every VACUUM, especially larger tables and very small
tables.

The emphasis is now on keeping each table's age reasonably recent over
time, across multiple successive VACUUM operations, while spreading out
the burden of freezing, avoiding big spikes.  Freezing is now primarily
treated as an overhead of long term storage of tuples in physical heap
pages.  There is less emphasis on the role freezing plays in preventing
the system from reaching the point of an xidStopLimit outage.

Now every VACUUM might need to wait for a cleanup lock, though few will.
It can only happen when required to advance relfrozenxid to no less than
half way between the existing relfrozenxid and nextXID.  In general
there is no telling how long VACUUM might spend waiting for a cleanup
lock, so it's usually more useful to focus on keeping up with freezing
at the level of the whole table.  VACUUM can afford to set relfrozenxid
to a significantly older value in the short term, since there are now
more opportunities to advance relfrozenxid in the long term.

Author: Peter Geoghegan <pg@bowt.ie>
Reviewed-By: Jeff Davis <pgsql@j-davis.com>
Reviewed-By: Andres Freund <andres@anarazel.de>
Discussion: https://postgr.es/m/CAH2-WzkU42GzrsHhL2BiC1QMhaVGmVdb5HR0_qczz0Gu2aSn=A@mail.gmail.com
---
 src/include/access/heapam.h                   |   2 +-
 src/include/commands/vacuum.h                 |  12 +-
 src/backend/access/heap/heapam.c              |  24 +-
 src/backend/access/heap/vacuumlazy.c          | 163 ++--
 src/backend/access/transam/multixact.c        |   5 +-
 src/backend/commands/cluster.c                |   3 +-
 src/backend/commands/vacuum.c                 | 138 ++--
 src/backend/utils/activity/pgstat_relation.c  |   4 +-
 src/backend/utils/misc/guc_tables.c           |   8 +-
 src/backend/utils/misc/postgresql.conf.sample |   9 +-
 doc/src/sgml/config.sgml                      | 103 +--
 doc/src/sgml/logicaldecoding.sgml             |   2 +-
 doc/src/sgml/maintenance.sgml                 | 721 ++++++++----------
 doc/src/sgml/ref/create_table.sgml            |   2 +-
 doc/src/sgml/ref/prepare_transaction.sgml     |   2 +-
 doc/src/sgml/ref/vacuum.sgml                  |  18 +-
 doc/src/sgml/ref/vacuumdb.sgml                |   4 +-
 .../expected/vacuum-no-cleanup-lock.out       |  24 +-
 .../specs/vacuum-no-cleanup-lock.spec         |  30 +-
 src/test/regress/expected/reloptions.out      |   8 +-
 src/test/regress/sql/reloptions.sql           |   8 +-
 21 files changed, 656 insertions(+), 634 deletions(-)

diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h
index 57d824740..961c8f76b 100644
--- a/src/include/access/heapam.h
+++ b/src/include/access/heapam.h
@@ -229,7 +229,7 @@ extern bool heap_freeze_tuple(HeapTupleHeader tuple,
 							  TransactionId relfrozenxid, TransactionId relminmxid,
 							  TransactionId FreezeLimit, TransactionId MultiXactCutoff);
 extern bool heap_tuple_should_freeze(HeapTupleHeader tuple,
-									 const struct VacuumCutoffs *cutoffs,
+									 const struct VacuumCutoffs *cutoffs, bool MinCutoffs,
 									 TransactionId *NewRelfrozenXid,
 									 MultiXactId *NewRelminMxid);
 extern bool heap_tuple_needs_eventual_freeze(HeapTupleHeader tuple);
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 122fb93e2..a4ed7674e 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -276,6 +276,13 @@ struct VacuumCutoffs
 	TransactionId FreezeLimit;
 	MultiXactId MultiXactCutoff;
 
+	/*
+	 * Earliest permissible NewRelfrozenXid/NewRelminMxid values that can be
+	 * set in pg_class at the end of VACUUM.
+	 */
+	TransactionId MinXid;
+	MultiXactId MinMulti;
+
 	/*
 	 * Threshold cutoff point (expressed in # of physical heap rel blocks in
 	 * rel's main fork) for triggering eager/all-visible freezing strategy
@@ -335,8 +342,9 @@ extern void vac_update_relstats(Relation relation,
 								bool *frozenxid_updated,
 								bool *minmulti_updated,
 								bool in_outer_xact);
-extern bool vacuum_set_xid_limits(Relation rel, const VacuumParams *params,
-								  struct VacuumCutoffs *cutoffs);
+extern void vacuum_set_xid_limits(Relation rel, const VacuumParams *params,
+								  struct VacuumCutoffs *cutoffs,
+								  double *antiwrapfrac);
 extern bool vacuum_xid_failsafe_check(TransactionId relfrozenxid,
 									  MultiXactId relminmxid);
 extern void vac_update_datfrozenxid(void);
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index caa34bd35..c1608b1ec 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -6303,7 +6303,7 @@ FreezeMultiXactId(MultiXactId multi, HeapTupleHeader tuple,
 	 */
 	axid = cutoffs->OldestXmin;
 	amxid = cutoffs->OldestMxact;
-	Assert(heap_tuple_should_freeze(tuple, cutoffs, &axid, &amxid));
+	Assert(heap_tuple_should_freeze(tuple, cutoffs, false, &axid, &amxid));
 
 	nnewmembers = 0;
 	newmembers = palloc(sizeof(MultiXactMember) * nmembers);
@@ -6784,7 +6784,7 @@ heap_prepare_freeze_tuple(HeapTupleHeader tuple,
 									   xmax_already_frozen))
 	{
 		pagefrz->freeze_required =
-			heap_tuple_should_freeze(tuple, cutoffs,
+			heap_tuple_should_freeze(tuple, cutoffs, false,
 									 &pagefrz->NoFreezeNewRelfrozenXid,
 									 &pagefrz->NoFreezeNewRelminMxid);
 	}
@@ -7350,13 +7350,19 @@ heap_tuple_needs_eventual_freeze(HeapTupleHeader tuple)
  * We must also deal with dead tuples here, since (xmin, xmax, xvac) fields
  * could be processed by pruning away the whole tuple instead of freezing.
  *
+ * Callers that specify 'MinCutoffs=false' have us apply the same FreezeLimit
+ * and MultiXactCutoff cutoffs used in heap_prepare_freeze_tuple.  Otherwise
+ * we use MinXid and MinMulti cutoffs, which are earlier cutoffs that VACUUM
+ * must always advance relfrozenxid/relminmxid up to, even when that means
+ * that it has to wait on a cleanup lock.
+ *
  * The *NewRelfrozenXid and *NewRelminMxid input/output arguments work just
  * like the similar fields from the FreezeCutoffs struct.  We never freeze
  * here, which makes tracking the oldest extant XID/MXID simple.
  */
 bool
 heap_tuple_should_freeze(HeapTupleHeader tuple,
-						 const struct VacuumCutoffs *cutoffs,
+						 const struct VacuumCutoffs *cutoffs, bool MinCutoffs,
 						 TransactionId *NewRelfrozenXid,
 						 MultiXactId *NewRelminMxid)
 {
@@ -7366,8 +7372,16 @@ heap_tuple_should_freeze(HeapTupleHeader tuple,
 	MultiXactId multi;
 	bool		freeze = false;
 
-	MustFreezeLimit = cutoffs->FreezeLimit;
-	MustFreezeMultiLimit = cutoffs->MultiXactCutoff;
+	if (!MinCutoffs)
+	{
+		MustFreezeLimit = cutoffs->FreezeLimit;
+		MustFreezeMultiLimit = cutoffs->MultiXactCutoff;
+	}
+	else
+	{
+		MustFreezeLimit = cutoffs->MinXid;
+		MustFreezeMultiLimit = cutoffs->MinMulti;
+	}
 
 	/* First deal with xmin */
 	xid = HeapTupleHeaderGetXmin(tuple);
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index a1984d68e..e6c2ff89f 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -109,10 +109,11 @@
 	((BlockNumber) (((uint64) 8 * 1024 * 1024 * 1024) / BLCKSZ))
 
 /*
- * Threshold that controls whether non-aggressive VACUUMs will skip any
- * all-visible pages when using the lazy freezing strategy
+ * Thresholds that control whether VACUUM will skip any all-visible pages when
+ * using the lazy freezing strategy
  */
 #define SKIPALLVIS_THRESHOLD_PAGES	0.05	/* i.e. 5% of rel_pages */
+#define SKIPALLVIS_MIDPOINT_THRESHOLD_PAGES	0.15
 
 /*
  * Size of the prefetch window for lazy vacuum backwards truncation scan.
@@ -148,9 +149,7 @@ typedef struct LVRelState
 	BufferAccessStrategy bstrategy;
 	ParallelVacuumState *pvs;
 
-	/* Aggressive VACUUM? (must set relfrozenxid >= FreezeLimit) */
-	bool		aggressive;
-	/* Skip (don't scan) all-visible pages? (must be !aggressive) */
+	/* Skip (don't scan) all-visible pages? */
 	bool		skipallvis;
 	/* Skip (don't scan) all-frozen pages? */
 	bool		skipallfrozen;
@@ -246,7 +245,7 @@ typedef struct LVSavedErrInfo
 
 /* non-export function prototypes */
 static void lazy_scan_heap(LVRelState *vacrel);
-static BlockNumber lazy_scan_strategy(LVRelState *vacrel,
+static BlockNumber lazy_scan_strategy(LVRelState *vacrel, double antiwrapfrac,
 									  BlockNumber all_visible,
 									  BlockNumber all_frozen);
 static BlockNumber lazy_scan_skip(LVRelState *vacrel,
@@ -312,10 +311,10 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	LVRelState *vacrel;
 	bool		verbose,
 				instrument,
-				aggressive,
 				frozenxid_updated,
 				minmulti_updated;
 	struct VacuumCutoffs cutoffs;
+	double		antiwrapfrac;
 	BlockNumber orig_rel_pages,
 				all_visible,
 				all_frozen,
@@ -354,7 +353,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	 * Get cutoffs that determine which deleted tuples are considered DEAD,
 	 * not just RECENTLY_DEAD, and which XIDs/MXIDs to freeze
 	 */
-	aggressive = vacuum_set_xid_limits(rel, params, &cutoffs);
+	vacuum_set_xid_limits(rel, params, &cutoffs, &antiwrapfrac);
 
 	/*
 	 * Setup error traceback support for ereport() first.  The idea is to set
@@ -404,7 +403,6 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	Assert(params->index_cleanup != VACOPTVALUE_UNSPECIFIED);
 	Assert(params->truncate != VACOPTVALUE_UNSPECIFIED &&
 		   params->truncate != VACOPTVALUE_AUTO);
-	vacrel->aggressive = aggressive;
 	vacrel->skipallvis = false; /* arbitrary initial value */
 	/* skipallfrozen indicates DISABLE_PAGE_SKIPPING to lazy_scan_strategy */
 	vacrel->skipallfrozen = (params->options & VACOPT_DISABLE_PAGE_SKIPPING) == 0;
@@ -489,7 +487,8 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	 */
 	vacrel->vmsnap = visibilitymap_snap(rel, orig_rel_pages,
 										&all_visible, &all_frozen);
-	scanned_pages = lazy_scan_strategy(vacrel, all_visible, all_frozen);
+	scanned_pages = lazy_scan_strategy(vacrel, antiwrapfrac,
+									   all_visible, all_frozen);
 	if (verbose)
 		ereport(INFO,
 				(errmsg("vacuuming \"%s.%s.%s\"",
@@ -550,25 +549,21 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	/*
 	 * Prepare to update rel's pg_class entry.
 	 *
-	 * Aggressive VACUUMs must always be able to advance relfrozenxid to a
-	 * value >= FreezeLimit, and relminmxid to a value >= MultiXactCutoff.
-	 * Non-aggressive VACUUMs may advance them by any amount, or not at all.
+	 * VACUUM can only advance relfrozenxid to a value >= MinXid, and
+	 * relminmxid to a value >= MinMulti.
 	 */
 	Assert(vacrel->NewRelfrozenXid == cutoffs.OldestXmin ||
-		   TransactionIdPrecedesOrEquals(aggressive ? cutoffs.FreezeLimit :
-										 vacrel->cutoffs.relfrozenxid,
+		   TransactionIdPrecedesOrEquals(cutoffs.MinXid,
 										 vacrel->NewRelfrozenXid));
 	Assert(vacrel->NewRelminMxid == cutoffs.OldestMxact ||
-		   MultiXactIdPrecedesOrEquals(aggressive ? cutoffs.MultiXactCutoff :
-									   vacrel->cutoffs.relminmxid,
+		   MultiXactIdPrecedesOrEquals(cutoffs.MinMulti,
 									   vacrel->NewRelminMxid));
 	if (vacrel->skipallvis)
 	{
 		/*
-		 * Must keep original relfrozenxid in a non-aggressive VACUUM whose
-		 * lazy_scan_strategy call determined it would skip all-visible pages
+		 * Must keep original relfrozenxid when lazy_scan_strategy call
+		 * decided to skip all-visible pages
 		 */
-		Assert(!aggressive);
 		vacrel->NewRelfrozenXid = InvalidTransactionId;
 		vacrel->NewRelminMxid = InvalidMultiXactId;
 	}
@@ -644,23 +639,11 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 				Assert(!params->is_wraparound);
 				msgfmt = _("finished vacuuming \"%s.%s.%s\": index scans: %d\n");
 			}
-			else if (params->is_wraparound)
-			{
-				/*
-				 * While it's possible for a VACUUM to be both is_wraparound
-				 * and !aggressive, that's just a corner-case -- is_wraparound
-				 * implies aggressive.  Produce distinct output for the corner
-				 * case all the same, just in case.
-				 */
-				if (aggressive)
-					msgfmt = _("automatic aggressive vacuum to prevent wraparound of table \"%s.%s.%s\": index scans: %d\n");
-				else
-					msgfmt = _("automatic vacuum to prevent wraparound of table \"%s.%s.%s\": index scans: %d\n");
-			}
 			else
 			{
-				if (aggressive)
-					msgfmt = _("automatic aggressive vacuum of table \"%s.%s.%s\": index scans: %d\n");
+				Assert(IsAutoVacuumWorkerProcess());
+				if (params->is_wraparound)
+					msgfmt = _("automatic vacuum to prevent wraparound of table \"%s.%s.%s\": index scans: %d\n");
 				else
 					msgfmt = _("automatic vacuum of table \"%s.%s.%s\": index scans: %d\n");
 			}
@@ -994,7 +977,6 @@ lazy_scan_heap(LVRelState *vacrel)
 			 * lazy_scan_noprune could not do all required processing.  Wait
 			 * for a cleanup lock, and call lazy_scan_prune in the usual way.
 			 */
-			Assert(vacrel->aggressive);
 			LockBuffer(buf, BUFFER_LOCK_UNLOCK);
 			LockBufferForCleanup(buf);
 		}
@@ -1255,19 +1237,16 @@ lazy_scan_heap(LVRelState *vacrel)
  * one VACUUM operation should need to freeze disproportionately many pages.
  *
  * Also determines if the ongoing VACUUM operation should skip all-visible
- * pages when advancing relfrozenxid is optional.  When VACUUM freezes eagerly
- * it always also scans pages eagerly, since it's important that relfrozenxid
- * advance in affected tables, which are larger.  When VACUUM freezes lazily
- * it might make sense to scan pages lazily (skip all-visible pages) or
- * eagerly (be capable of relfrozenxid advancement), depending on the extra
- * cost - we might need to scan only a few extra pages.
+ * pages when advancing relfrozenxid is still optional (before target rel has
+ * attained an age that forces an antiwraparound autovacuum).  Decision is
+ * based in part on caller's antiwrapfrac argument, which represents how close
+ * the table age is to forcing antiwraparound autovacuum.
  *
  * Returns final scanned_pages for the VACUUM operation.
  */
 static BlockNumber
-lazy_scan_strategy(LVRelState *vacrel,
-				   BlockNumber all_visible,
-				   BlockNumber all_frozen)
+lazy_scan_strategy(LVRelState *vacrel, double antiwrapfrac,
+				   BlockNumber all_visible, BlockNumber all_frozen)
 {
 	BlockNumber rel_pages = vacrel->rel_pages,
 				scanned_pages_skipallvis,
@@ -1326,9 +1305,6 @@ lazy_scan_strategy(LVRelState *vacrel,
 		BlockNumber nextra,
 					nextra_threshold;
 
-		/* VACUUM of small table -- use lazy freeze strategy */
-		vacrel->eager_freeze_strategy = false;
-
 		/*
 		 * Decide on whether or not we'll skip all-visible pages.
 		 *
@@ -1342,21 +1318,51 @@ lazy_scan_strategy(LVRelState *vacrel,
 		 * that way, so be lazy (just skip) unless the added cost is very low.
 		 * We opt for a skipallfrozen-only VACUUM when the number of extra
 		 * pages (extra scanned pages that are all-visible but not all-frozen)
-		 * is less than 5% of rel_pages (or 32 pages when rel_pages is small).
+		 * is less than 5% of rel_pages (or 32 pages when rel_pages is small)
+		 * if relfrozenxid has yet to attain an age that uses 50% of the XID
+		 * space available before the GUC cutoff for antiwraparound
+		 * autovacuum.  A more aggressive threshold of 15% is used when
+		 * relfrozenxid is older than that.
 		 */
 		nextra = scanned_pages_skipallfrozen - scanned_pages_skipallvis;
-		nextra_threshold = (double) rel_pages * SKIPALLVIS_THRESHOLD_PAGES;
+
+		if (antiwrapfrac < 0.5)
+			nextra_threshold = (double) rel_pages *
+				SKIPALLVIS_THRESHOLD_PAGES;
+		else
+			nextra_threshold = (double) rel_pages *
+				SKIPALLVIS_MIDPOINT_THRESHOLD_PAGES;
+
 		nextra_threshold = Max(32, nextra_threshold);
 
-		/* Only skipallvis when DISABLE_PAGE_SKIPPING not in use */
-		vacrel->skipallvis = nextra >= nextra_threshold &&
-			vacrel->skipallfrozen && !vacrel->aggressive;
+		/*
+		 * We must advance relfrozenxid when it already attained an age that
+		 * consumes >= 90% of the available XID space (or MXID space) before
+		 * the crossover point for antiwraparound autovacuum.
+		 *
+		 * Also use eager freezing strategy when we're past the "90% towards
+		 * wraparound" point, even though the table size is below the usual
+		 * eager_threshold table size cutoff.  The added cost is usually not
+		 * too great.  We may be able to fall into a pattern of continually
+		 * advancing relfrozenxid this way.
+		 */
+		if (antiwrapfrac < 0.9)
+		{
+			/* Only skipallvis when DISABLE_PAGE_SKIPPING not in use */
+			vacrel->skipallvis = nextra >= nextra_threshold &&
+				vacrel->skipallfrozen;
+			vacrel->eager_freeze_strategy = false;
+		}
+		else
+		{
+			vacrel->skipallvis = false;
+			vacrel->eager_freeze_strategy = true;
+		}
 	}
 
 	/* Return the appropriate variant of scanned_pages */
 	if (vacrel->skipallvis)
 	{
-		Assert(!vacrel->aggressive);
 		Assert(vacrel->skipallfrozen);
 		return scanned_pages_skipallvis;
 	}
@@ -1983,11 +1989,13 @@ retry:
  * operation left LP_DEAD items behind.  We'll at least collect any such items
  * in the dead_items array for removal from indexes.
  *
- * For aggressive VACUUM callers, we may return false to indicate that a full
- * cleanup lock is required for processing by lazy_scan_prune.  This is only
- * necessary when the aggressive VACUUM needs to freeze some tuple XIDs from
- * one or more tuples on the page.  We always return true for non-aggressive
- * callers.
+ * We may return false to indicate that a full cleanup lock is required for
+ * processing by lazy_scan_prune.  This is only necessary when VACUUM needs to
+ * freeze some tuple XIDs from one or more tuples on the page.  This should
+ * only happen when multiple successive VACUUM operations all fail to get a
+ * cleanup lock on the same heap page (assuming default or at least typical
+ * freeze settings).  Waiting for a cleanup lock should be avoided unless it's
+ * the only way to advance relfrozenxid by enough to satisfy autovacuum.c.
  *
  * See lazy_scan_prune for an explanation of hastup return flag.
  * recordfreespace flag instructs caller on whether or not it should do
@@ -2054,35 +2062,24 @@ lazy_scan_noprune(LVRelState *vacrel,
 
 		*hastup = true;			/* page prevents rel truncation */
 		tupleheader = (HeapTupleHeader) PageGetItem(page, itemid);
-		if (heap_tuple_should_freeze(tupleheader, &vacrel->cutoffs,
+		if (heap_tuple_should_freeze(tupleheader, &vacrel->cutoffs, true,
 									 &NewRelfrozenXid, &NewRelminMxid))
 		{
-			/* Tuple with XID < FreezeLimit (or MXID < MultiXactCutoff) */
-			if (vacrel->aggressive)
-			{
-				/*
-				 * Aggressive VACUUMs must always be able to advance rel's
-				 * relfrozenxid to a value >= FreezeLimit (and be able to
-				 * advance rel's relminmxid to a value >= MultiXactCutoff).
-				 * The ongoing aggressive VACUUM won't be able to do that
-				 * unless it can freeze an XID (or MXID) from this tuple now.
-				 *
-				 * The only safe option is to have caller perform processing
-				 * of this page using lazy_scan_prune.  Caller might have to
-				 * wait a while for a cleanup lock, but it can't be helped.
-				 */
-				vacrel->offnum = InvalidOffsetNumber;
-				return false;
-			}
-
 			/*
-			 * Non-aggressive VACUUMs are under no obligation to advance
-			 * relfrozenxid (even by one XID).  We can be much laxer here.
+			 * Tuple with XID < MinXid (or MXID < MinMulti)
 			 *
-			 * Currently we always just accept an older final relfrozenxid
-			 * and/or relminmxid value.  We never make caller wait or work a
-			 * little harder, even when it likely makes sense to do so.
+			 * VACUUM must always be able to advance rel's relfrozenxid and
+			 * relminmxid to minimum values.  The ongoing VACUUM won't be able
+			 * to do that unless it can freeze an XID (or MXID) from this
+			 * tuple now.
+			 *
+			 * The only safe option is to have caller perform processing of
+			 * this page using lazy_scan_prune.  Caller might have to wait a
+			 * long time for a cleanup lock, which can be very disruptive, but
+			 * it can't be helped.
 			 */
+			vacrel->offnum = InvalidOffsetNumber;
+			return false;
 		}
 
 		ItemPointerSet(&(tuple.t_self), blkno, offnum);
diff --git a/src/backend/access/transam/multixact.c b/src/backend/access/transam/multixact.c
index 204aa9504..ba575c5fd 100644
--- a/src/backend/access/transam/multixact.c
+++ b/src/backend/access/transam/multixact.c
@@ -2816,10 +2816,7 @@ ReadMultiXactCounts(uint32 *multixacts, MultiXactOffset *members)
  * freeze table and the minimum freeze age based on the effective
  * autovacuum_multixact_freeze_max_age this function returns.  In the worst
  * case, we'll claim the freeze_max_age to zero, and every vacuum of any
- * table will try to freeze every multixact.
- *
- * It's possible that these thresholds should be user-tunable, but for now
- * we keep it simple.
+ * table will freeze every multixact.
  */
 int
 MultiXactMemberFreezeThreshold(void)
diff --git a/src/backend/commands/cluster.c b/src/backend/commands/cluster.c
index 6cfea04a9..c2f708e33 100644
--- a/src/backend/commands/cluster.c
+++ b/src/backend/commands/cluster.c
@@ -825,6 +825,7 @@ copy_table_data(Oid OIDNewHeap, Oid OIDOldHeap, Oid OIDOldIndex, bool verbose,
 	TupleDesc	newTupDesc PG_USED_FOR_ASSERTS_ONLY;
 	VacuumParams params;
 	struct VacuumCutoffs cutoffs;
+	double		antiwrapfrac;
 	bool		use_sort;
 	double		num_tuples = 0,
 				tups_vacuumed = 0,
@@ -913,7 +914,7 @@ copy_table_data(Oid OIDNewHeap, Oid OIDOldHeap, Oid OIDOldIndex, bool verbose,
 	 * not to be aggressive about this.
 	 */
 	memset(&params, 0, sizeof(VacuumParams));
-	vacuum_set_xid_limits(OldHeap, &params, &cutoffs);
+	vacuum_set_xid_limits(OldHeap, &params, &cutoffs, &antiwrapfrac);
 
 	/*
 	 * FreezeXid will become the table's new relfrozenxid, and that mustn't go
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index ffa8eac12..cd0684f44 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -937,24 +937,33 @@ get_all_vacuum_rels(int options)
  *
  * The target relation and VACUUM parameters are our inputs.
  *
- * Output parameters are the cutoffs that VACUUM caller should use.
+ * Output parameters are the cutoffs that VACUUM caller should use, and
+ * antiwrapfrac, which indicates how close table is to requiring that
+ * autovacuum.c launch an antiwraparound autovacuum.
+ *
+ * The antiwrapfrac value 1.0 represents the point that autovacuum.c
+ * scheduling considers advancing relfrozenxid strictly necessary.  Values
+ * between 0.0 and 1.0 represent how close the table is to the point of
+ * mandatory relfrozenxid/relminmxid advancement.
  */
-bool
+void
 vacuum_set_xid_limits(Relation rel, const VacuumParams *params,
-					  struct VacuumCutoffs *cutoffs)
+					  struct VacuumCutoffs *cutoffs, double *antiwrapfrac)
 {
 	int			freeze_min_age,
 				multixact_freeze_min_age,
 				freeze_table_age,
 				multixact_freeze_table_age,
 				effective_multixact_freeze_max_age,
-				freeze_strategy_threshold;
+				freeze_strategy_threshold,
+				relfrozenxid_age,
+				relminmxid_age;
 	TransactionId nextXID,
-				safeOldestXmin,
-				aggressiveXIDCutoff;
+				safeOldestXmin;
 	MultiXactId nextMXID,
-				safeOldestMxact,
-				aggressiveMXIDCutoff;
+				safeOldestMxact;
+	double		XIDFrac,
+				MXIDFrac;
 
 	/* Determining table age details  */
 	cutoffs->relfrozenxid = rel->rd_rel->relfrozenxid;
@@ -1084,6 +1093,74 @@ vacuum_set_xid_limits(Relation rel, const VacuumParams *params,
 		freeze_strategy_threshold = vacuum_freeze_strategy_threshold;
 	cutoffs->freeze_strategy_threshold = freeze_strategy_threshold;
 
+	/*
+	 * Work out how close we are to needing an antiwraparound VACUUM.
+	 *
+	 * Determine the table freeze age to use: as specified by the caller, or
+	 * the value of the vacuum_freeze_table_age GUC.  The GUC's default value
+	 * of -1 is interpreted as "just use autovacuum_freeze_max_age value".
+	 * Also clamp using autovacuum_freeze_max_age.
+	 */
+	if (freeze_table_age < 0)
+		freeze_table_age = vacuum_freeze_table_age;
+	if (freeze_table_age < 0 || freeze_table_age > autovacuum_freeze_max_age)
+		freeze_table_age = autovacuum_freeze_max_age;
+
+	/*
+	 * Similar to the above, determine the table freeze age to use for
+	 * multixacts: as specified by the caller, or the value of the
+	 * vacuum_multixact_freeze_table_age GUC.   The GUC's default value of -1
+	 * is interpreted as "just use effective_multixact_freeze_max_age value".
+	 * Also clamp using effective_multixact_freeze_max_age.
+	 */
+	if (multixact_freeze_table_age < 0)
+		multixact_freeze_table_age = vacuum_multixact_freeze_table_age;
+	if (multixact_freeze_table_age < 0 ||
+		multixact_freeze_table_age > effective_multixact_freeze_max_age)
+		multixact_freeze_table_age = effective_multixact_freeze_max_age;
+
+	/* Final antiwrapfrac can come from either XID or MXID table age */
+	relfrozenxid_age = Max(nextXID - rel->rd_rel->relfrozenxid, 1);
+	relminmxid_age = Max(nextMXID - rel->rd_rel->relminmxid, 1);
+	freeze_table_age = Max(freeze_table_age, 1);
+	multixact_freeze_table_age = Max(multixact_freeze_table_age, 1);
+	XIDFrac = (double) relfrozenxid_age / (double) freeze_table_age;
+	MXIDFrac = (double) relminmxid_age / (double) multixact_freeze_table_age;
+	*antiwrapfrac = Max(XIDFrac, MXIDFrac);
+
+	/*
+	 * Make sure that antiwraparound autovacuums reliably advance relfrozenxid
+	 * to the satisfaction of autovacuum.c, even when the reloption version of
+	 * autovacuum_freeze_max_age happens to be in use
+	 */
+	if (params->is_wraparound)
+		*antiwrapfrac = 1.0;
+
+	/*
+	 * Pages that caller can cleanup lock immediately will never be left with
+	 * XIDs < FreezeLimit (nor with MXIDs < MultiXactCutoff).  Determine
+	 * values for a distinct set of cutoffs applied to pages that cannot be
+	 * immediately cleanup locked.  The cutoffs govern caller's wait behavior.
+	 *
+	 * It is safer to accept earlier final relfrozenxid and relminmxid values
+	 * than it would be to wait indefinitely for a cleanup lock.  Waiting for
+	 * a cleanup lock to freeze one heap page risks not freezing every other
+	 * eligible heap page.  Keeping up the momentum is what matters most.
+	 */
+	cutoffs->MinXid = nextXID - (freeze_table_age / 2);
+	if (!TransactionIdIsNormal(cutoffs->MinXid))
+		cutoffs->MinXid = FirstNormalTransactionId;
+	/* MinXid must always be <= FreezeLimit */
+	if (TransactionIdPrecedes(cutoffs->FreezeLimit, cutoffs->MinXid))
+		cutoffs->MinXid = cutoffs->FreezeLimit;
+
+	cutoffs->MinMulti = nextMXID - (multixact_freeze_table_age / 2);
+	if (cutoffs->MinMulti < FirstMultiXactId)
+		cutoffs->MinMulti = FirstMultiXactId;
+	/* MinMulti must always be <= MultiXactCutoff */
+	if (MultiXactIdPrecedes(cutoffs->MultiXactCutoff, cutoffs->MinMulti))
+		cutoffs->MinMulti = cutoffs->MultiXactCutoff;
+
 	/*
 	 * Assert that all cutoff invariants hold.
 	 *
@@ -1095,47 +1172,10 @@ vacuum_set_xid_limits(Relation rel, const VacuumParams *params,
 										 cutoffs->OldestXmin));
 	Assert(MultiXactIdPrecedesOrEquals(cutoffs->MultiXactCutoff,
 									   cutoffs->OldestMxact));
-	/*
-	 * Determine the table freeze age to use: as specified by the caller, or
-	 * the value of the vacuum_freeze_table_age GUC, but in any case not more
-	 * than autovacuum_freeze_max_age * 0.95, so that if you have e.g nightly
-	 * VACUUM schedule, the nightly VACUUM gets a chance to freeze XIDs before
-	 * anti-wraparound autovacuum is launched.
-	 */
-	if (freeze_table_age < 0)
-		freeze_table_age = vacuum_freeze_table_age;
-	freeze_table_age = Min(freeze_table_age, autovacuum_freeze_max_age * 0.95);
-	Assert(freeze_table_age >= 0);
-	aggressiveXIDCutoff = nextXID - freeze_table_age;
-	if (!TransactionIdIsNormal(aggressiveXIDCutoff))
-		aggressiveXIDCutoff = FirstNormalTransactionId;
-	if (TransactionIdPrecedesOrEquals(rel->rd_rel->relfrozenxid,
-									  aggressiveXIDCutoff))
-		return true;
-
-	/*
-	 * Similar to the above, determine the table freeze age to use for
-	 * multixacts: as specified by the caller, or the value of the
-	 * vacuum_multixact_freeze_table_age GUC, but in any case not more than
-	 * effective_multixact_freeze_max_age * 0.95, so that if you have e.g.
-	 * nightly VACUUM schedule, the nightly VACUUM gets a chance to freeze
-	 * multixacts before anti-wraparound autovacuum is launched.
-	 */
-	if (multixact_freeze_table_age < 0)
-		multixact_freeze_table_age = vacuum_multixact_freeze_table_age;
-	multixact_freeze_table_age =
-		Min(multixact_freeze_table_age,
-			effective_multixact_freeze_max_age * 0.95);
-	Assert(multixact_freeze_table_age >= 0);
-	aggressiveMXIDCutoff = nextMXID - multixact_freeze_table_age;
-	if (aggressiveMXIDCutoff < FirstMultiXactId)
-		aggressiveMXIDCutoff = FirstMultiXactId;
-	if (MultiXactIdPrecedesOrEquals(rel->rd_rel->relminmxid,
-									aggressiveMXIDCutoff))
-		return true;
-
-	/* Non-aggressive VACUUM */
-	return false;
+	Assert(TransactionIdPrecedesOrEquals(cutoffs->MinXid,
+										 cutoffs->FreezeLimit));
+	Assert(MultiXactIdPrecedesOrEquals(cutoffs->MinMulti,
+									   cutoffs->MultiXactCutoff));
 }
 
 /*
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index f92e16e7a..12dfafd67 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -235,8 +235,8 @@ pgstat_report_vacuum(Oid tableoid, bool shared,
 	tabentry->n_dead_tuples = deadtuples;
 
 	/*
-	 * It is quite possible that a non-aggressive VACUUM ended up skipping
-	 * various pages, however, we'll zero the insert counter here regardless.
+	 * It is quite possible that VACUUM will skip all-visible pages for a
+	 * smaller table, however, we'll zero the insert counter here regardless.
 	 * It's currently used only to track when we need to perform an "insert"
 	 * autovacuum, which are mainly intended to freeze newly inserted tuples.
 	 * Zeroing this may just mean we'll not try to vacuum the table again
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index d3c8ae87d..939e4b5aa 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -2476,10 +2476,10 @@ struct config_int ConfigureNamesInt[] =
 	{
 		{"vacuum_freeze_table_age", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Age at which VACUUM should scan whole table to freeze tuples."),
-			NULL
+			gettext_noop("-1 to use autovacuum_freeze_max_age value.")
 		},
 		&vacuum_freeze_table_age,
-		150000000, 0, 2000000000,
+		-1, -1, 2000000000,
 		NULL, NULL, NULL
 	},
 
@@ -2496,10 +2496,10 @@ struct config_int ConfigureNamesInt[] =
 	{
 		{"vacuum_multixact_freeze_table_age", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Multixact age at which VACUUM should scan whole table to freeze tuples."),
-			NULL
+			gettext_noop("-1 to use autovacuum_multixact_freeze_max_age value.")
 		},
 		&vacuum_multixact_freeze_table_age,
-		150000000, 0, 2000000000,
+		-1, -1, 2000000000,
 		NULL, NULL, NULL
 	},
 
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index a409e6281..01f37b493 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -659,6 +659,13 @@
 					# autovacuum, -1 means use
 					# vacuum_cost_limit
 
+# - AUTOVACUUM compatibility options (legacy) -
+
+#vacuum_freeze_table_age = -1	# target maximum XID age, or -1 to
+					# use autovacuum_freeze_max_age
+#vacuum_multixact_freeze_table_age = -1	# target maximum MXID age, or -1 to
+					# use autovacuum_multixact_freeze_max_age
+
 
 #------------------------------------------------------------------------------
 # CLIENT CONNECTION DEFAULTS
@@ -692,11 +699,9 @@
 #lock_timeout = 0			# in milliseconds, 0 is disabled
 #idle_in_transaction_session_timeout = 0	# in milliseconds, 0 is disabled
 #idle_session_timeout = 0		# in milliseconds, 0 is disabled
-#vacuum_freeze_table_age = 150000000
 #vacuum_freeze_strategy_threshold = 4GB
 #vacuum_freeze_min_age = 50000000
 #vacuum_failsafe_age = 1600000000
-#vacuum_multixact_freeze_table_age = 150000000
 #vacuum_multixact_freeze_min_age = 5000000
 #vacuum_multixact_failsafe_age = 1600000000
 #bytea_output = 'hex'			# hex, escape
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 9c5861bd7..938603283 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8215,7 +8215,7 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         Note that even when this parameter is disabled, the system
         will launch autovacuum processes if necessary to
         prevent transaction ID wraparound.  See <xref
-        linkend="vacuum-for-wraparound"/> for more information.
+        linkend="vacuum-xid-space"/> for more information.
        </para>
       </listitem>
      </varlistentry>
@@ -8404,7 +8404,7 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         This parameter can only be set at server start, but the setting
         can be reduced for individual tables by
         changing table storage parameters.
-        For more information see <xref linkend="vacuum-for-wraparound"/>.
+        For more information see <xref linkend="vacuum-xid-space"/>.
        </para>
       </listitem>
      </varlistentry>
@@ -9120,31 +9120,6 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
-     <varlistentry id="guc-vacuum-freeze-table-age" xreflabel="vacuum_freeze_table_age">
-      <term><varname>vacuum_freeze_table_age</varname> (<type>integer</type>)
-      <indexterm>
-       <primary><varname>vacuum_freeze_table_age</varname> configuration parameter</primary>
-      </indexterm>
-      </term>
-      <listitem>
-       <para>
-        <command>VACUUM</command> performs an aggressive scan if the table's
-        <structname>pg_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
-        tuples.  The default is 150 million transactions.  Although users can
-        set this value anywhere from zero to two billion, <command>VACUUM</command>
-        will silently limit the effective value to 95% of
-        <xref linkend="guc-autovacuum-freeze-max-age"/>, so that a
-        periodic manual <command>VACUUM</command> has a chance to run before an
-        anti-wraparound autovacuum is launched for the table. For more
-        information see
-        <xref linkend="vacuum-for-wraparound"/>.
-       </para>
-      </listitem>
-     </varlistentry>
-
      <varlistentry id="guc-vacuum-freeze-strategy-threshold" xreflabel="vacuum_freeze_strategy_threshold">
       <term><varname>vacuum_freeze_strategy_threshold</varname> (<type>integer</type>)
       <indexterm>
@@ -9160,6 +9135,39 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-vacuum-freeze-table-age" xreflabel="vacuum_freeze_table_age">
+      <term><varname>vacuum_freeze_table_age</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>vacuum_freeze_table_age</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        <command>VACUUM</command> reliably advances
+        <structfield>relfrozenxid</structfield> to a recent value if
+        the table's
+        <structname>pg_class</structname>.<structfield>relfrozenxid</structfield>
+        field has reached the age specified by this setting.
+        The default is -1.  If -1 is specified, the value
+        of <xref linkend="guc-autovacuum-freeze-max-age"/> is used.
+        Although users can set this value anywhere from zero to two
+        billion, <command>VACUUM</command> will silently limit the
+        effective value to <xref
+         linkend="guc-autovacuum-freeze-max-age"/>. For more
+        information see <xref linkend="vacuum-xid-space"/>.
+       </para>
+       <note>
+        <para>
+         The meaning of this parameter, and its default value, changed
+         in <productname>PostgreSQL</productname> 16.  Freezing and advancing
+         <structname>pg_class</structname>.<structfield>relfrozenxid</structfield>
+         now take place more proactively, in every
+         <command>VACUUM</command> operation.
+        </para>
+       </note>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-vacuum-freeze-min-age" xreflabel="vacuum_freeze_min_age">
       <term><varname>vacuum_freeze_min_age</varname> (<type>integer</type>)
       <indexterm>
@@ -9179,7 +9187,7 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         the value of <xref linkend="guc-autovacuum-freeze-max-age"/>, so
         that there is not an unreasonably short time between forced
         autovacuums.  For more information see <xref
-        linkend="vacuum-for-wraparound"/>.
+        linkend="vacuum-xid-space"/>.
        </para>
       </listitem>
      </varlistentry>
@@ -9225,19 +9233,27 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </term>
       <listitem>
        <para>
-        <command>VACUUM</command> performs an aggressive scan if the table's
-        <structname>pg_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
-        tuples.  The default is 150 million multixacts.
-        Although users can set this value anywhere from zero to two billion,
-        <command>VACUUM</command> will silently limit the effective value to 95% of
-        <xref linkend="guc-autovacuum-multixact-freeze-max-age"/>, so that a
-        periodic manual <command>VACUUM</command> has a chance to run before an
-        anti-wraparound is launched for the table.
-        For more information see <xref linkend="vacuum-for-multixact-wraparound"/>.
+       <command>VACUUM</command> reliably advances
+       <structfield>relminmxid</structfield> to a recent value if the table's
+       <structname>pg_class</structname>.<structfield>relminmxid</structfield>
+       field has reached the age specified by this setting.
+       The default is -1.  If -1 is specified, the value of <xref
+        linkend="guc-autovacuum-multixact-freeze-max-age"/> is used.
+       Although users can set this value anywhere from zero to two
+       billion, <command>VACUUM</command> will silently limit the
+       effective value to <xref
+        linkend="guc-autovacuum-multixact-freeze-max-age"/>. For more
+       information see <xref linkend="vacuum-xid-space"/>.
        </para>
+       <note>
+        <para>
+         The meaning of this parameter, and its default value, changed
+         in <productname>PostgreSQL</productname> 16.  Freezing and advancing
+         <structname>pg_class</structname>.<structfield>relminmxid</structfield>
+         now take place more proactively, in every
+         <command>VACUUM</command> operation.
+        </para>
+       </note>
       </listitem>
      </varlistentry>
 
@@ -9249,10 +9265,9 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </term>
       <listitem>
        <para>
-        Specifies the cutoff age (in multixacts) that
-        <command>VACUUM</command> should use to decide whether to
-        trigger freezing of pages with an older multixact ID.  The
-        default is 5 million multixacts.
+        Specifies the cutoff age (in multixacts) that <command>VACUUM</command>
+        should use to decide whether to trigger freezing of pages with
+        an older multixact ID.  The default is 5 million multixacts.
         Although users can set this value anywhere from zero to one billion,
         <command>VACUUM</command> will silently limit the effective value to half
         the value of <xref linkend="guc-autovacuum-multixact-freeze-max-age"/>,
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index 38ee69dcc..380da3c1e 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -324,7 +324,7 @@ postgres=# select * from pg_logical_slot_get_changes('regression_slot', NULL, NU
       because neither required WAL nor required rows from the system catalogs
       can be removed by <command>VACUUM</command> as long as they are required by a replication
       slot.  In extreme cases this could cause the database to shut down to prevent
-      transaction ID wraparound (see <xref linkend="vacuum-for-wraparound"/>).
+      transaction ID wraparound (see <xref linkend="vacuum-xid-space"/>).
       So if a slot is no longer required it should be dropped.
      </para>
     </caution>
diff --git a/doc/src/sgml/maintenance.sgml b/doc/src/sgml/maintenance.sgml
index 554b3a75d..ed54a2988 100644
--- a/doc/src/sgml/maintenance.sgml
+++ b/doc/src/sgml/maintenance.sgml
@@ -400,202 +400,73 @@
    </para>
   </sect2>
 
-  <sect2 id="vacuum-for-wraparound">
-   <title>Preventing Transaction ID Wraparound Failures</title>
+  <sect2 id="freezing">
+   <title>Freezing tuples</title>
 
-   <indexterm zone="vacuum-for-wraparound">
-    <primary>transaction ID</primary>
-    <secondary>wraparound</secondary>
-   </indexterm>
+   <para>
+    <command>VACUUM</command> freezes a page's tuples (by processing
+    the tuple header fields described in <xref
+     linkend="storage-tuple-layout"/>) as a way of avoiding long term
+    dependencies on transaction status metadata referenced therein.
+    Heap pages that only contain frozen tuples are suitable for long
+    term storage.  Larger databases are often mostly comprised of cold
+    data that is modified very infrequently, plus a relatively small
+    amount of hot data that is updated far more frequently.
+    <command>VACUUM</command> applies a variety of techniques that
+    allow it to concentrate most of its efforts on hot data.
+   </para>
+
+   <sect3 id="vacuum-xid-space">
+    <title>Managing the 32-bit Transaction ID address space</title>
 
     <indexterm>
      <primary>wraparound</primary>
      <secondary>of transaction IDs</secondary>
     </indexterm>
 
-   <para>
-    <productname>PostgreSQL</productname>'s
-    <link linkend="mvcc-intro">MVCC</link> transaction semantics
-    depend on being able to compare transaction ID (<acronym>XID</acronym>)
-    numbers: a row version with an insertion XID greater than the current
-    transaction's XID is <quote>in the future</quote> and should not be visible
-    to the current transaction.  But since transaction IDs have limited size
-    (32 bits) a cluster that runs for a long time (more
-    than 4 billion transactions) would suffer <firstterm>transaction ID
-    wraparound</firstterm>: the XID counter wraps around to zero, and all of a sudden
-    transactions that were in the past appear to be in the future &mdash; which
-    means their output become invisible.  In short, catastrophic data loss.
-    (Actually the data is still there, but that's cold comfort if you cannot
-    get at it.)  To avoid this, it is necessary to vacuum every table
-    in every database at least once every two billion transactions.
-   </para>
-
-   <para>
-    The reason that periodic vacuuming solves the problem is that
-    <command>VACUUM</command> will mark rows as <emphasis>frozen</emphasis>, indicating that
-    they were inserted by a transaction that committed sufficiently far in
-    the past that the effects of the inserting transaction are certain to be
-    visible to all current and future transactions.
-    Normal XIDs are
-    compared using modulo-2<superscript>32</superscript> arithmetic. This means
-    that for every normal XID, there are two billion XIDs that are
-    <quote>older</quote> and two billion that are <quote>newer</quote>; another
-    way to say it is that the normal XID space is circular with no
-    endpoint. Therefore, once a row version has been created with a particular
-    normal XID, the row version will appear to be <quote>in the past</quote> for
-    the next two billion transactions, no matter which normal XID we are
-    talking about. If the row version still exists after more than two billion
-    transactions, it will suddenly appear to be in the future. To
-    prevent this, <productname>PostgreSQL</productname> reserves a special XID,
-    <literal>FrozenTransactionId</literal>, which does not follow the normal XID
-    comparison rules and is always considered older
-    than every normal XID.
-    Frozen row versions are treated as if the inserting XID were
-    <literal>FrozenTransactionId</literal>, so that they will appear to be
-    <quote>in the past</quote> to all normal transactions regardless of wraparound
-    issues, and so such row versions will be valid until deleted, no matter
-    how long that is.
-   </para>
-
-   <note>
     <para>
-     In <productname>PostgreSQL</productname> versions before 9.4, freezing was
-     implemented by actually replacing a row's insertion XID
-     with <literal>FrozenTransactionId</literal>, which was visible in the
-     row's <structname>xmin</structname> system column.  Newer versions just set a flag
-     bit, preserving the row's original <structname>xmin</structname> for possible
-     forensic use.  However, rows with <structname>xmin</structname> equal
-     to <literal>FrozenTransactionId</literal> (2) may still be found
-     in databases <application>pg_upgrade</application>'d from pre-9.4 versions.
+     <productname>PostgreSQL</productname>'s <link
+      linkend="mvcc-intro">MVCC</link> transaction semantics depend on
+     being able to compare transaction ID (<acronym>XID</acronym>)
+     numbers: a row version with an insertion XID greater than the
+     current transaction's XID is <quote>in the future</quote> and
+     should not be visible to the current transaction.  But since the
+     on-disk representation of transaction IDs is only 32-bits, the
+     system is incapable of representing
+     <emphasis>distances</emphasis> between any two XIDs that exceed
+     about 2 billion transaction IDs.
     </para>
+
     <para>
-     Also, system catalogs may contain rows with <structname>xmin</structname> equal
-     to <literal>BootstrapTransactionId</literal> (1), indicating that they were
-     inserted during the first phase of <application>initdb</application>.
-     Like <literal>FrozenTransactionId</literal>, this special XID is treated as
-     older than every normal XID.
+     One of the purposes of periodic vacuuming is to manage the
+     Transaction Id address space.  <command>VACUUM</command> will
+     mark rows as <emphasis>frozen</emphasis>, indicating that they
+     were inserted by a transaction that committed sufficiently far in
+     the past that the effects of the inserting transaction are
+     certain to be visible to all current and future transactions.
+     There is, in effect, an infinite distance between a frozen
+     transaction ID and any unfrozen transaction ID.  This allows the
+     on-disk representation of transaction IDs to recycle the 32-bit
+     address space efficiently.
     </para>
-   </note>
 
-   <para>
-    <xref linkend="guc-vacuum-freeze-min-age"/>
-    controls how old an XID value has to be before rows bearing that XID will be
-    frozen.  Increasing this setting may avoid unnecessary work if the
-    rows that would otherwise be frozen will soon be modified again,
-    but decreasing this setting increases
-    the number of transactions that can elapse before the table must be
-    vacuumed again.
-   </para>
-
-   <para>
-    <command>VACUUM</command> uses the <link linkend="storage-vm">visibility map</link>
-    to determine which pages of a table must be scanned.  Normally, it
-    will skip pages that don't have any dead row versions even if those pages
-    might still have row versions with old XID values.  Therefore, normal
-    <command>VACUUM</command>s won't always freeze every old row version in the table.
-    When that happens, <command>VACUUM</command> will eventually need to perform an
-    <firstterm>aggressive vacuum</firstterm>, which will freeze all eligible unfrozen
-    XID and MXID values, including those from all-visible but not all-frozen pages.
-    In practice most tables require periodic aggressive vacuuming.
-    <xref linkend="guc-vacuum-freeze-table-age"/>
-    controls when <command>VACUUM</command> does that: all-visible but not all-frozen
-    pages are scanned if the number of transactions that have passed since the
-    last such scan is greater than <varname>vacuum_freeze_table_age</varname> minus
-    <varname>vacuum_freeze_min_age</varname>. Setting
-    <varname>vacuum_freeze_table_age</varname> to 0 forces <command>VACUUM</command> to
-    always use its aggressive strategy.
-   </para>
-
-   <para>
-    The maximum time that a table can go unvacuumed is two billion
-    transactions minus the <varname>vacuum_freeze_min_age</varname> value at
-    the time of the last aggressive vacuum. If it were to go
-    unvacuumed for longer than
-    that, data loss could result.  To ensure that this does not happen,
-    autovacuum is invoked on any table that might contain unfrozen rows with
-    XIDs older than the age specified by the configuration parameter <xref
-    linkend="guc-autovacuum-freeze-max-age"/>.  (This will happen even if
-    autovacuum is disabled.)
-   </para>
-
-   <para>
-    This implies that if a table is not otherwise vacuumed,
-    autovacuum will be invoked on it approximately once every
-    <varname>autovacuum_freeze_max_age</varname> minus
-    <varname>vacuum_freeze_min_age</varname> transactions.
-    For tables that are regularly vacuumed for space reclamation purposes,
-    this is of little importance.  However, for static tables
-    (including tables that receive inserts, but no updates or deletes),
-    there is no need to vacuum for space reclamation, so it can
-    be useful to try to maximize the interval between forced autovacuums
-    on very large static tables.  Obviously one can do this either by
-    increasing <varname>autovacuum_freeze_max_age</varname> or decreasing
-    <varname>vacuum_freeze_min_age</varname>.
-   </para>
-
-   <para>
-    The effective maximum for <varname>vacuum_freeze_table_age</varname> is 0.95 *
-    <varname>autovacuum_freeze_max_age</varname>; a setting higher than that will be
-    capped to the maximum. A value higher than
-    <varname>autovacuum_freeze_max_age</varname> wouldn't make sense because an
-    anti-wraparound autovacuum would be triggered at that point anyway, and
-    the 0.95 multiplier leaves some breathing room to run a manual
-    <command>VACUUM</command> before that happens.  As a rule of thumb,
-    <command>vacuum_freeze_table_age</command> should be set to a value somewhat
-    below <varname>autovacuum_freeze_max_age</varname>, leaving enough gap so that
-    a regularly scheduled <command>VACUUM</command> or an autovacuum triggered by
-    normal delete and update activity is run in that window.  Setting it too
-    close could lead to anti-wraparound autovacuums, even though the table
-    was recently vacuumed to reclaim space, whereas lower values lead to more
-    frequent aggressive vacuuming.
-   </para>
-
-   <para>
-    The sole disadvantage of increasing <varname>autovacuum_freeze_max_age</varname>
-    (and <varname>vacuum_freeze_table_age</varname> along with it) is that
-    the <filename>pg_xact</filename> and <filename>pg_commit_ts</filename>
-    subdirectories of the database cluster will take more space, because it
-    must store the commit status and (if <varname>track_commit_timestamp</varname> is
-    enabled) timestamp of all transactions back to
-    the <varname>autovacuum_freeze_max_age</varname> horizon.  The commit status uses
-    two bits per transaction, so if
-    <varname>autovacuum_freeze_max_age</varname> is set to its maximum allowed value
-    of two billion, <filename>pg_xact</filename> can be expected to grow to about half
-    a gigabyte and <filename>pg_commit_ts</filename> to about 20GB.  If this
-    is trivial compared to your total database size,
-    setting <varname>autovacuum_freeze_max_age</varname> to its maximum allowed value
-    is recommended.  Otherwise, set it depending on what you are willing to
-    allow for <filename>pg_xact</filename> and <filename>pg_commit_ts</filename> storage.
-    (The default, 200 million transactions, translates to about 50MB
-    of <filename>pg_xact</filename> storage and about 2GB of <filename>pg_commit_ts</filename>
-    storage.)
-   </para>
-
-   <para>
-    One disadvantage of decreasing <varname>vacuum_freeze_min_age</varname> is that
-    it might cause <command>VACUUM</command> to do useless work: freezing a row
-    version is a waste of time if the row is modified
-    soon thereafter (causing it to acquire a new XID).  So the setting should
-    be large enough that rows are not frozen until they are unlikely to change
-    any more.
-   </para>
-
-   <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
-    <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
-    XID at the end of the most recent <command>VACUUM</command> that successfully
-    advanced <structfield>relfrozenxid</structfield>.  All rows inserted by
-    transactions older than this cutoff XID are guaranteed to have been frozen.
-    Similarly, the <structfield>datfrozenxid</structfield> column of a database's
-    <structname>pg_database</structname> row is a lower bound on the unfrozen XIDs
-    appearing in that database &mdash; it is just the minimum of the
-    per-table <structfield>relfrozenxid</structfield> values within the database.
-    A convenient way to
-    examine this information is to execute queries such as:
+    <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
+     <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 XID at the end of the most recent
+     <command>VACUUM</command>.  All rows inserted by transactions
+     older than this cutoff XID are guaranteed to have been frozen.
+     Similarly, the <structfield>datfrozenxid</structfield> column of
+     a database's <structname>pg_database</structname> row is a lower
+     bound on the unfrozen XIDs appearing in that database &mdash; it
+     is just the minimum of the per-table
+     <structfield>relfrozenxid</structfield> values within the
+     database.  A convenient way to examine this information is to
+     execute queries such as:
 
 <programlisting>
 SELECT c.oid::regclass as table_name,
@@ -607,83 +478,13 @@ WHERE c.relkind IN ('r', 'm');
 SELECT datname, age(datfrozenxid) FROM pg_database;
 </programlisting>
 
-    The <literal>age</literal> column measures the number of transactions from the
-    cutoff XID to the current transaction's XID.
-   </para>
-
-   <tip>
-    <para>
-     When the <command>VACUUM</command> command's <literal>VERBOSE</literal>
-     parameter is specified, <command>VACUUM</command> prints various
-     statistics about the table.  This includes information about how
-     <structfield>relfrozenxid</structfield> and
-     <structfield>relminmxid</structfield> advanced.  The same details appear
-     in the server log when autovacuum logging (controlled by <xref
-      linkend="guc-log-autovacuum-min-duration"/>) reports on a
-     <command>VACUUM</command> operation executed by autovacuum.
+     The <literal>age</literal> column measures the number of transactions from the
+     cutoff XID to the current transaction's XID.
     </para>
-   </tip>
-
-   <para>
-    <command>VACUUM</command> normally only scans pages that have been modified
-    since the last vacuum, but <structfield>relfrozenxid</structfield> can only be
-    advanced when every page of the table
-    that might contain unfrozen XIDs is scanned.  This happens when
-    <structfield>relfrozenxid</structfield> is more than
-    <varname>vacuum_freeze_table_age</varname> transactions old, when
-    <command>VACUUM</command>'s <literal>FREEZE</literal> option is used, or when all
-    pages that are not already all-frozen happen to
-    require vacuuming to remove dead row versions. When <command>VACUUM</command>
-    scans every page in the table that is not already all-frozen, it should
-    set <literal>age(relfrozenxid)</literal> to a value just a little more than the
-    <varname>vacuum_freeze_min_age</varname> setting
-    that was used (more by the number of transactions started since the
-    <command>VACUUM</command> started).  <command>VACUUM</command>
-    will set <structfield>relfrozenxid</structfield> to the oldest XID
-    that remains in the table, so it's possible that the final value
-    will be much more recent than strictly required.
-    If no <structfield>relfrozenxid</structfield>-advancing
-    <command>VACUUM</command> is issued on the table until
-    <varname>autovacuum_freeze_max_age</varname> is reached, an autovacuum will soon
-    be forced for the table.
-   </para>
-
-   <para>
-    If for some reason autovacuum fails to clear old XIDs from a table, the
-    system will begin to emit warning messages like this when the database's
-    oldest XIDs reach forty million transactions from the wraparound point:
-
-<programlisting>
-WARNING:  database "mydb" must be vacuumed within 39985967 transactions
-HINT:  To avoid a database shutdown, execute a database-wide VACUUM in that database.
-</programlisting>
-
-    (A manual <command>VACUUM</command> should fix the problem, as suggested by the
-    hint; but note that the <command>VACUUM</command> must be performed by a
-    superuser, else it will fail to process system catalogs and thus not
-    be able to advance the database's <structfield>datfrozenxid</structfield>.)
-    If these warnings are
-    ignored, the system will shut down and refuse to start any new
-    transactions once there are fewer than three million transactions left
-    until wraparound:
-
-<programlisting>
-ERROR:  database is not accepting commands to avoid wraparound data loss in database "mydb"
-HINT:  Stop the postmaster and vacuum that database in single-user mode.
-</programlisting>
-
-    The three-million-transaction safety margin exists to let the
-    administrator recover without data loss, by manually executing the
-    required <command>VACUUM</command> commands.  However, since the system will not
-    execute commands once it has gone into the safety shutdown mode,
-    the only way to do this is to stop the server and start the server in single-user
-    mode to execute <command>VACUUM</command>.  The shutdown mode is not enforced
-    in single-user mode.  See the <xref linkend="app-postgres"/> reference
-    page for details about using single-user mode.
-   </para>
+   </sect3>
 
    <sect3 id="vacuum-for-multixact-wraparound">
-    <title>Multixacts and Wraparound</title>
+    <title>Managing the 32-bit MultiXactId address space</title>
 
     <indexterm>
      <primary>MultiXactId</primary>
@@ -704,47 +505,109 @@ HINT:  Stop the postmaster and vacuum that database in single-user mode.
      particular multixact ID is stored separately in
      the <filename>pg_multixact</filename> subdirectory, and only the multixact ID
      appears in the <structfield>xmax</structfield> field in the tuple header.
-     Like transaction IDs, multixact IDs are implemented as a
-     32-bit counter and corresponding storage, all of which requires
-     careful aging management, storage cleanup, and wraparound handling.
-     There is a separate storage area which holds the list of members in
-     each multixact, which also uses a 32-bit counter and which must also
-     be managed.
+     Like transaction IDs, multixact IDs are implemented as a 32-bit
+     counter and corresponding storage.
     </para>
 
     <para>
-     Whenever <command>VACUUM</command> scans any part of a table, it will replace
-     any multixact ID it encounters which is older than
-     <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
-     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.
+     A separate <structfield>relminmxid</structfield> field can be
+     advanced any time <structfield>relfrozenxid</structfield> is
+     advanced.  <command>VACUUM</command> manages the MultiXactId
+     address space by implementing rules that are analogous to the
+     approach taken with Transaction IDs.  Many of the XID-based
+     settings that influence <command>VACUUM</command>'s behavior have
+     direct MultiXactId analogs. A convenient way to examine
+     information about the MultiXactId address space is to execute
+     queries such as:
+    </para>
+<programlisting>
+SELECT c.oid::regclass as table_name,
+       mxid_age(c.relminmxid)
+FROM pg_class c
+WHERE c.relkind IN ('r', 'm');
+
+SELECT datname, mxid_age(datminmxid) FROM pg_database;
+</programlisting>
+   </sect3>
+
+   <sect3 id="freezing-strategies">
+    <title>Lazy and eager freezing strategies</title>
+    <para>
+     When <command>VACUUM</command> is configured to freeze more
+     aggressively it will typically set the table's
+     <structfield>relfrozenxid</structfield> and
+     <structfield>relminmxid</structfield> fields to relatively recent
+     values.  However, there can be significant variation among tables
+     with varying workload characteristics.  There can even be
+     variation in how <structfield>relfrozenxid</structfield>
+     advancement takes place over time for the same table, across
+     successive <command>VACUUM</command> operations.  Sometimes
+     <command>VACUUM</command> will be able to advance
+     <structfield>relfrozenxid</structfield> and
+     <structfield>relminmxid</structfield> by relatively many
+     XIDs/MXIDs despite performing relatively little freezing work.  On
+     the other hand <command>VACUUM</command> can sometimes freeze many
+     individual pages while only advancing
+     <structfield>relfrozenxid</structfield> by as few as one or two
+     XIDs (this is typically seen following bulk loading).
     </para>
 
-    <para>
-     Aggressive <command>VACUUM</command>s, regardless of what causes
-     them, are <emphasis>guaranteed</emphasis> to be able to advance
-     the table's <structfield>relminmxid</structfield>.
-     Eventually, as all tables in all databases are scanned and their
-     oldest multixact values are advanced, on-disk storage for older
-     multixacts can be removed.
-    </para>
+    <tip>
+     <para>
+      When the <command>VACUUM</command> command's <literal>VERBOSE</literal>
+      parameter is specified, <command>VACUUM</command> prints various
+      statistics about the table.  This includes information about how
+      <structfield>relfrozenxid</structfield> and
+      <structfield>relminmxid</structfield> advanced, as well as
+      information about how many pages were newly frozen.  The same
+      details appear in the server log when autovacuum logging
+      (controlled by <xref linkend="guc-log-autovacuum-min-duration"/>)
+      reports on a <command>VACUUM</command> operation executed by
+      autovacuum.
+     </para>
+    </tip>
 
     <para>
-     As a safety device, an aggressive vacuum scan will
-     occur for any table whose multixact-age is greater than <xref
-     linkend="guc-autovacuum-multixact-freeze-max-age"/>.  Also, if the
-     storage occupied by multixacts members exceeds 2GB, aggressive vacuum
-     scans will occur more often for all tables, starting with those that
-     have the oldest multixact-age.  Both of these kinds of aggressive
-     scans will occur even if autovacuum is nominally disabled.
+     As a general rule, the design of <command>VACUUM</command>
+     prioritizes stable and predictable performance characteristics
+     over time, while still leaving some scope for freezing lazily when
+     a lazy strategy is likely to avoid unnecessary work altogether.  Tables
+     whose heap relation on-disk size is less than <xref
+      linkend="guc-vacuum-freeze-strategy-threshold"/> at the start of
+     <command>VACUUM</command> will have page freezing triggered based
+     on <quote>lazy</quote> criteria.  Freezing will only take place
+     when one or more XIDs attain an age greater than <xref
+      linkend="guc-vacuum-freeze-min-age"/>, or when one or more MXIDs
+     attain an age greater than <xref
+      linkend="guc-vacuum-multixact-freeze-min-age"/>.
+    </para>
+    <para>
+     Tables that are larger than <xref
+      linkend="guc-vacuum-freeze-strategy-threshold"/> will have
+     <command>VACUUM</command> trigger freezing for any and all pages
+     that are eligible to be frozen under the lazy criteria, as well as
+     pages that <command>VACUUM</command> considers all visible pages.
+     This is the eager freezing strategy.  The design makes the soft
+     assumption that larger tables will tend to consist of pages that
+     will only need to be processed by <command>VACUUM</command> once.
+     The overhead of freezing each page is expected to be slightly
+     higher in the short term, but much lower in the long term, at
+     least on average.  Eager freezing also limits the accumulation of
+     unfrozen pages, which tends to improve performance
+     <emphasis>stability</emphasis> over time.
+    </para>
+    <para>
+     Occasionally, <command>VACUUM</command> is required to advance
+     <structfield>relfrozenxid</structfield> and/or
+     <structfield>relminmxid</structfield> up to a specific value
+     to ensure the system always has a healthy amount of usable
+     transaction ID address space.  This usually only occurs when
+     <command>VACUUM</command> must be run by autovacuum specifically
+     for the purpose of advancing <structfield>relfrozenxid</structfield>,
+     when no <command>VACUUM</command> has been triggered for some
+     time.  In practice most individual tables will consistently have
+     somewhat recent values through routine vacuuming to clean up old
+     row versions.
     </para>
    </sect3>
   </sect2>
@@ -802,117 +665,197 @@ HINT:  Stop the postmaster and vacuum that database in single-user mode.
     <xref linkend="guc-superuser-reserved-connections"/> limits.
    </para>
 
-   <para>
-    Tables whose <structfield>relfrozenxid</structfield> value is more than
-    <xref linkend="guc-autovacuum-freeze-max-age"/> transactions old are always
-    vacuumed (this also applies to those tables whose freeze max age has
-    been modified via storage parameters; see below).  Otherwise, if the
-    number of tuples obsoleted since the last
-    <command>VACUUM</command> exceeds the <quote>vacuum threshold</quote>, the
-    table is vacuumed.  The vacuum threshold is defined as:
+   <sect3 id="triggering-thresholds">
+    <title>Triggering thresholds</title>
+    <para>
+     Tables whose <structfield>relfrozenxid</structfield> value is
+     more than <xref linkend="guc-autovacuum-freeze-max-age"/>
+     transactions old are always vacuumed (this also applies to those
+     tables whose freeze max age has been modified via storage
+     parameters; see below).  Otherwise, if the number of tuples
+     obsoleted since the last <command>VACUUM</command> exceeds the
+     <quote>vacuum threshold</quote>, the table is vacuumed.  The
+     vacuum threshold is defined as:
 <programlisting>
 vacuum threshold = vacuum base threshold + vacuum scale factor * number of tuples
 </programlisting>
-    where the vacuum base threshold is
-    <xref linkend="guc-autovacuum-vacuum-threshold"/>,
-    the vacuum scale factor is
-    <xref linkend="guc-autovacuum-vacuum-scale-factor"/>,
+    where the vacuum base threshold is <xref
+     linkend="guc-autovacuum-vacuum-threshold"/>, the vacuum scale
+    factor is <xref linkend="guc-autovacuum-vacuum-scale-factor"/>,
     and the number of tuples is
     <structname>pg_class</structname>.<structfield>reltuples</structfield>.
-   </para>
+    </para>
 
-   <para>
-    The table is also vacuumed if the number of tuples inserted since the last
-    vacuum has exceeded the defined insert threshold, which is defined as:
+    <para>
+     The table is also vacuumed if the number of tuples inserted since
+     the last vacuum has exceeded the defined insert threshold, which
+     is defined as:
 <programlisting>
 vacuum insert threshold = vacuum base insert threshold + vacuum insert scale factor * number of tuples
 </programlisting>
-    where the vacuum insert base threshold is
-    <xref linkend="guc-autovacuum-vacuum-insert-threshold"/>,
-    and vacuum insert scale factor is
-    <xref linkend="guc-autovacuum-vacuum-insert-scale-factor"/>.
-    Such vacuums may allow portions of the table to be marked as
-    <firstterm>all visible</firstterm> and also allow tuples to be frozen, which
-    can reduce the work required in subsequent vacuums.
-    For tables which receive <command>INSERT</command> operations but no or
-    almost no <command>UPDATE</command>/<command>DELETE</command> operations,
-    it may be beneficial to lower the table's
-    <xref linkend="reloption-autovacuum-freeze-min-age"/> as this may allow
-    tuples to be frozen by earlier vacuums.  The number of obsolete tuples and
-    the number of inserted tuples are obtained from the cumulative statistics system;
-    it is a semi-accurate count updated by each <command>UPDATE</command>,
-    <command>DELETE</command> and <command>INSERT</command> operation.  (It is
-    only semi-accurate because some information might be lost under heavy
-    load.)  If the <structfield>relfrozenxid</structfield> value of the table
-    is more than <varname>vacuum_freeze_table_age</varname> transactions old,
-    an aggressive vacuum is performed to freeze old tuples and advance
-    <structfield>relfrozenxid</structfield>; otherwise, only pages that have been modified
-    since the last vacuum are scanned.
-   </para>
+     where the vacuum insert base threshold
+     is <xref linkend="guc-autovacuum-vacuum-insert-threshold"/>, and
+     vacuum insert scale factor is <xref
+      linkend="guc-autovacuum-vacuum-insert-scale-factor"/>.  Such
+     vacuums may allow portions of the table to be marked as
+     <firstterm>all visible</firstterm> and also allow tuples to be
+     frozen.  The number of obsolete tuples and the number of inserted
+     tuples are obtained from the cumulative statistics system; it is
+     a semi-accurate count updated by each <command>UPDATE</command>,
+     <command>DELETE</command> and <command>INSERT</command>
+     operation.  (It is only semi-accurate because some information
+     might be lost under heavy load.)
+    </para>
 
-   <para>
-    For analyze, a similar condition is used: the threshold, defined as:
+    <para>
+     For analyze, a similar condition is used: the threshold, defined as:
 <programlisting>
 analyze threshold = analyze base threshold + analyze scale factor * number of tuples
 </programlisting>
-    is compared to the total number of tuples inserted, updated, or deleted
-    since the last <command>ANALYZE</command>.
-   </para>
-
-   <para>
-    Partitioned tables are not processed by autovacuum.  Statistics
-    should be collected by running a manual <command>ANALYZE</command> when it is
-    first populated, and again whenever the distribution of data in its
-    partitions changes significantly.
-   </para>
-
-   <para>
-    Temporary tables cannot be accessed by autovacuum.  Therefore,
-    appropriate vacuum and analyze operations should be performed via
-    session SQL commands.
-   </para>
-
-   <para>
-    The default thresholds and scale factors are taken from
-    <filename>postgresql.conf</filename>, but it is possible to override them
-    (and many other autovacuum control parameters) on a per-table basis; see
-    <xref linkend="sql-createtable-storage-parameters"/> for more information.
-    If a setting has been changed via a table's storage parameters, that value
-    is used when processing that table; otherwise the global settings are
-    used. See <xref linkend="runtime-config-autovacuum"/> for more details on
-    the global settings.
-   </para>
-
-   <para>
-    When multiple workers are running, the autovacuum cost delay parameters
-    (see <xref linkend="runtime-config-resource-vacuum-cost"/>) are
-    <quote>balanced</quote> among all the running workers, so that the
-    total I/O impact on the system is the same regardless of the number
-    of workers actually running.  However, any workers processing tables whose
-    per-table <literal>autovacuum_vacuum_cost_delay</literal> or
-    <literal>autovacuum_vacuum_cost_limit</literal> storage parameters have been set
-    are not considered in the balancing algorithm.
-   </para>
-
-   <para>
-    Autovacuum workers generally don't block other commands.  If a process
-    attempts to acquire a lock that conflicts with the
-    <literal>SHARE UPDATE EXCLUSIVE</literal> lock held by autovacuum, lock
-    acquisition will interrupt the autovacuum.  For conflicting lock modes,
-    see <xref linkend="table-lock-compatibility"/>.  However, if the autovacuum
-    is running to prevent transaction ID wraparound (i.e., the autovacuum query
-    name in the <structname>pg_stat_activity</structname> view ends with
-    <literal>(to prevent wraparound)</literal>), the autovacuum is not
-    automatically interrupted.
-   </para>
-
-   <warning>
-    <para>
-     Regularly running commands that acquire locks conflicting with a
-     <literal>SHARE UPDATE EXCLUSIVE</literal> lock (e.g., ANALYZE) can
-     effectively prevent autovacuums from ever completing.
+     is compared to the total number of tuples inserted, updated, or
+     deleted since the last <command>ANALYZE</command>.
     </para>
-   </warning>
+
+   </sect3>
+
+   <sect3 id="anti-wraparound">
+    <title>Anti-wraparound autovacuum</title>
+
+    <indexterm>
+     <primary>wraparound</primary>
+     <secondary>of transaction IDs</secondary>
+    </indexterm>
+
+    <indexterm>
+     <primary>wraparound</primary>
+     <secondary>of multixact IDs</secondary>
+    </indexterm>
+
+    <para>
+     If no <structfield>relfrozenxid</structfield>-advancing
+     <command>VACUUM</command> is issued on the table before
+     <varname>autovacuum_freeze_max_age</varname> is reached, an
+     anti-wraparound autovacuum will soon be launched against the
+     table.  This reliably advances
+     <structfield>relfrozenxid</structfield> when there is no other
+     reason for <command>VACUUM</command> to run, or when a smaller
+     table had <command>VACUUM</command> operations that lazily opted
+     not to advance <structfield>relfrozenxid</structfield>.
+    </para>
+
+    <para>
+     An anti-wraparound autovacuum will also be triggered for any
+     table whose multixact-age is greater than <xref
+      linkend="guc-autovacuum-multixact-freeze-max-age"/>.  However,
+     if the storage occupied by multixacts members exceeds 2GB,
+     anti-wraparound vacuum might occur more often than this.
+    </para>
+
+    <para>
+     If for some reason autovacuum fails to clear old XIDs from a table, the
+     system will begin to emit warning messages like this when the database's
+     oldest XIDs reach forty million transactions from the wraparound point:
+
+<programlisting>
+WARNING:  database "mydb" must be vacuumed within 39985967 transactions
+HINT:  To avoid a database shutdown, execute a database-wide VACUUM in that database.
+</programlisting>
+
+     (A manual <command>VACUUM</command> should fix the problem, as suggested by the
+     hint; but note that the <command>VACUUM</command> must be performed by a
+     superuser, else it will fail to process system catalogs and thus not
+     be able to advance the database's <structfield>datfrozenxid</structfield>.)
+     If these warnings are
+     ignored, the system will shut down and refuse to start any new
+     transactions once there are fewer than three million transactions left
+     until wraparound:
+
+<programlisting>
+ERROR:  database is not accepting commands to avoid wraparound data loss in database "mydb"
+HINT:  Stop the postmaster and vacuum that database in single-user mode.
+</programlisting>
+
+     The three-million-transaction safety margin exists to let the
+     administrator recover by manually executing the required
+     <command>VACUUM</command> commands.  It is usually sufficient to
+     allow autovacuum to finish against the table with the oldest
+     <structfield>relfrozenxid</structfield> and/or
+     <structfield>relminmxid</structfield> value.  The wraparound
+     failsafe mechanism controlled by <xref
+      linkend="guc-vacuum-failsafe-age"/> and <xref
+      linkend="guc-vacuum-multixact-failsafe-age"/> will typically
+     trigger before warning messages are first emitted.  This happens
+     dynamically, in any antiwraparound autovacuum worker that is
+     tasked with advancing very old table ages.  It will also happen
+     during manual <command>VACUUM</command> operations.
+    </para>
+
+    <para>
+     The shutdown mode is not enforced in single-user mode, which can
+     be useful in some disaster recovery scenarios.  See the <xref
+      linkend="app-postgres"/> reference page for details about using
+     single-user mode.
+    </para>
+   </sect3>
+
+   <sect3 id="Limitations">
+    <title>Limitations</title>
+
+    <para>
+     Partitioned tables are not processed by autovacuum.  Statistics
+     should be collected by running a manual <command>ANALYZE</command> when it is
+     first populated, and again whenever the distribution of data in its
+     partitions changes significantly.
+    </para>
+
+    <para>
+     Temporary tables cannot be accessed by autovacuum.  Therefore,
+     appropriate vacuum and analyze operations should be performed via
+     session SQL commands.
+    </para>
+
+    <para>
+     The default thresholds and scale factors are taken from
+     <filename>postgresql.conf</filename>, but it is possible to override them
+     (and many other autovacuum control parameters) on a per-table basis; see
+     <xref linkend="sql-createtable-storage-parameters"/> for more information.
+     If a setting has been changed via a table's storage parameters, that value
+     is used when processing that table; otherwise the global settings are
+     used. See <xref linkend="runtime-config-autovacuum"/> for more details on
+     the global settings.
+    </para>
+
+    <para>
+     When multiple workers are running, the autovacuum cost delay parameters
+     (see <xref linkend="runtime-config-resource-vacuum-cost"/>) are
+     <quote>balanced</quote> among all the running workers, so that the
+     total I/O impact on the system is the same regardless of the number
+     of workers actually running.  However, any workers processing tables whose
+     per-table <literal>autovacuum_vacuum_cost_delay</literal> or
+     <literal>autovacuum_vacuum_cost_limit</literal> storage parameters have been set
+     are not considered in the balancing algorithm.
+    </para>
+
+    <para>
+     Autovacuum workers generally don't block other commands.  If a process
+     attempts to acquire a lock that conflicts with the
+     <literal>SHARE UPDATE EXCLUSIVE</literal> lock held by autovacuum, lock
+     acquisition will interrupt the autovacuum.  For conflicting lock modes,
+     see <xref linkend="table-lock-compatibility"/>.  However, if the autovacuum
+     is running to prevent transaction ID wraparound (i.e., the autovacuum query
+     name in the <structname>pg_stat_activity</structname> view ends with
+     <literal>(to prevent wraparound)</literal>), the autovacuum is not
+     automatically interrupted.
+    </para>
+
+    <warning>
+     <para>
+      Regularly running commands that acquire locks conflicting with a
+      <literal>SHARE UPDATE EXCLUSIVE</literal> lock (e.g., ANALYZE) can
+      effectively prevent autovacuums from ever completing.
+     </para>
+    </warning>
+   </sect3>
   </sect2>
  </sect1>
 
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index eabbf9e65..859175718 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1503,7 +1503,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
      and/or <command>ANALYZE</command> operations on this table following the rules
      discussed in <xref linkend="autovacuum"/>.
      If false, this table will not be autovacuumed, except to prevent
-     transaction ID wraparound. See <xref linkend="vacuum-for-wraparound"/> for
+     transaction ID wraparound. See <xref linkend="vacuum-xid-space"/> for
      more about wraparound prevention.
      Note that the autovacuum daemon does not run at all (except to prevent
      transaction ID wraparound) if the <xref linkend="guc-autovacuum"/>
diff --git a/doc/src/sgml/ref/prepare_transaction.sgml b/doc/src/sgml/ref/prepare_transaction.sgml
index f4f6118ac..1817ed1e3 100644
--- a/doc/src/sgml/ref/prepare_transaction.sgml
+++ b/doc/src/sgml/ref/prepare_transaction.sgml
@@ -128,7 +128,7 @@ PREPARE TRANSACTION <replaceable class="parameter">transaction_id</replaceable>
     This will interfere with the ability of <command>VACUUM</command> to reclaim
     storage, and in extreme cases could cause the database to shut down
     to prevent transaction ID wraparound (see <xref
-    linkend="vacuum-for-wraparound"/>).  Keep in mind also that the transaction
+    linkend="vacuum-xid-space"/>).  Keep in mind also that the transaction
     continues to hold whatever locks it held.  The intended usage of the
     feature is that a prepared transaction will normally be committed or
     rolled back as soon as an external transaction manager has verified that
diff --git a/doc/src/sgml/ref/vacuum.sgml b/doc/src/sgml/ref/vacuum.sgml
index 78e35abb9..43ffbbbd3 100644
--- a/doc/src/sgml/ref/vacuum.sgml
+++ b/doc/src/sgml/ref/vacuum.sgml
@@ -119,14 +119,14 @@ VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] [ ANALYZE ] [ <replaceable class="paramet
     <term><literal>FREEZE</literal></term>
     <listitem>
      <para>
-      Selects aggressive <quote>freezing</quote> of tuples.
-      Specifying <literal>FREEZE</literal> is equivalent to performing
-      <command>VACUUM</command> with the
-      <xref linkend="guc-vacuum-freeze-min-age"/> and
-      <xref linkend="guc-vacuum-freeze-table-age"/> parameters
-      set to zero.  Aggressive freezing is always performed when the
-      table is rewritten, so this option is redundant when <literal>FULL</literal>
-      is specified.
+      Selects eager <quote>freezing</quote> of tuples.  Specifying
+      <literal>FREEZE</literal> is equivalent to performing
+      <command>VACUUM</command> with the <xref
+       linkend="guc-vacuum-freeze-min-age"/> and <xref
+       linkend="guc-vacuum-freeze-strategy-threshold"/> parameters set
+      to zero.  Eager freezing is always performed when the table is
+      rewritten, so this option is redundant when
+      <literal>FULL</literal> is specified.
      </para>
     </listitem>
    </varlistentry>
@@ -210,7 +210,7 @@ VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] [ ANALYZE ] [ <replaceable class="paramet
       there are many dead tuples in the table.  This may be useful
       when it is necessary to make <command>VACUUM</command> run as
       quickly as possible to avoid imminent transaction ID wraparound
-      (see <xref linkend="vacuum-for-wraparound"/>).  However, the
+      (see <xref linkend="vacuum-xid-space"/>).  However, the
       wraparound failsafe mechanism controlled by <xref
        linkend="guc-vacuum-failsafe-age"/>  will generally trigger
       automatically to avoid transaction ID wraparound failure, and
diff --git a/doc/src/sgml/ref/vacuumdb.sgml b/doc/src/sgml/ref/vacuumdb.sgml
index 841aced3b..48942c58f 100644
--- a/doc/src/sgml/ref/vacuumdb.sgml
+++ b/doc/src/sgml/ref/vacuumdb.sgml
@@ -180,7 +180,7 @@ PostgreSQL documentation
       <term><option>--freeze</option></term>
       <listitem>
        <para>
-        Aggressively <quote>freeze</quote> tuples.
+        Eagerly <quote>freeze</quote> tuples.
        </para>
       </listitem>
      </varlistentry>
@@ -259,7 +259,7 @@ PostgreSQL documentation
         transaction ID age of at least
         <replaceable class="parameter">xid_age</replaceable>.  This setting
         is useful for prioritizing tables to process to prevent transaction
-        ID wraparound (see <xref linkend="vacuum-for-wraparound"/>).
+        ID wraparound (see <xref linkend="vacuum-xid-space"/>).
        </para>
        <para>
         For the purposes of this option, the transaction ID age of a relation
diff --git a/src/test/isolation/expected/vacuum-no-cleanup-lock.out b/src/test/isolation/expected/vacuum-no-cleanup-lock.out
index f7bc93e8f..076fe07ab 100644
--- a/src/test/isolation/expected/vacuum-no-cleanup-lock.out
+++ b/src/test/isolation/expected/vacuum-no-cleanup-lock.out
@@ -1,6 +1,6 @@
 Parsed test spec with 4 sessions
 
-starting permutation: vacuumer_pg_class_stats dml_insert vacuumer_nonaggressive_vacuum vacuumer_pg_class_stats
+starting permutation: vacuumer_pg_class_stats dml_insert vacuumer_vacuum_noprune vacuumer_pg_class_stats
 step vacuumer_pg_class_stats: 
   SELECT relpages, reltuples FROM pg_class WHERE oid = 'smalltbl'::regclass;
 
@@ -12,7 +12,7 @@ relpages|reltuples
 step dml_insert: 
   INSERT INTO smalltbl SELECT max(id) + 1 FROM smalltbl;
 
-step vacuumer_nonaggressive_vacuum: 
+step vacuumer_vacuum_noprune: 
   VACUUM smalltbl;
 
 step vacuumer_pg_class_stats: 
@@ -24,7 +24,7 @@ relpages|reltuples
 (1 row)
 
 
-starting permutation: vacuumer_pg_class_stats dml_insert pinholder_cursor vacuumer_nonaggressive_vacuum vacuumer_pg_class_stats pinholder_commit
+starting permutation: vacuumer_pg_class_stats dml_insert pinholder_cursor vacuumer_vacuum_noprune vacuumer_pg_class_stats pinholder_commit
 step vacuumer_pg_class_stats: 
   SELECT relpages, reltuples FROM pg_class WHERE oid = 'smalltbl'::regclass;
 
@@ -46,7 +46,7 @@ dummy
     1
 (1 row)
 
-step vacuumer_nonaggressive_vacuum: 
+step vacuumer_vacuum_noprune: 
   VACUUM smalltbl;
 
 step vacuumer_pg_class_stats: 
@@ -61,7 +61,7 @@ step pinholder_commit:
   COMMIT;
 
 
-starting permutation: vacuumer_pg_class_stats pinholder_cursor dml_insert dml_delete dml_insert vacuumer_nonaggressive_vacuum vacuumer_pg_class_stats pinholder_commit
+starting permutation: vacuumer_pg_class_stats pinholder_cursor dml_insert dml_delete dml_insert vacuumer_vacuum_noprune vacuumer_pg_class_stats pinholder_commit
 step vacuumer_pg_class_stats: 
   SELECT relpages, reltuples FROM pg_class WHERE oid = 'smalltbl'::regclass;
 
@@ -89,7 +89,7 @@ step dml_delete:
 step dml_insert: 
   INSERT INTO smalltbl SELECT max(id) + 1 FROM smalltbl;
 
-step vacuumer_nonaggressive_vacuum: 
+step vacuumer_vacuum_noprune: 
   VACUUM smalltbl;
 
 step vacuumer_pg_class_stats: 
@@ -104,7 +104,7 @@ step pinholder_commit:
   COMMIT;
 
 
-starting permutation: vacuumer_pg_class_stats dml_insert dml_delete pinholder_cursor dml_insert vacuumer_nonaggressive_vacuum vacuumer_pg_class_stats pinholder_commit
+starting permutation: vacuumer_pg_class_stats dml_insert dml_delete pinholder_cursor dml_insert vacuumer_vacuum_noprune vacuumer_pg_class_stats pinholder_commit
 step vacuumer_pg_class_stats: 
   SELECT relpages, reltuples FROM pg_class WHERE oid = 'smalltbl'::regclass;
 
@@ -132,7 +132,7 @@ dummy
 step dml_insert: 
   INSERT INTO smalltbl SELECT max(id) + 1 FROM smalltbl;
 
-step vacuumer_nonaggressive_vacuum: 
+step vacuumer_vacuum_noprune: 
   VACUUM smalltbl;
 
 step vacuumer_pg_class_stats: 
@@ -147,7 +147,7 @@ step pinholder_commit:
   COMMIT;
 
 
-starting permutation: dml_begin dml_other_begin dml_key_share dml_other_key_share vacuumer_nonaggressive_vacuum pinholder_cursor dml_other_update dml_commit dml_other_commit vacuumer_nonaggressive_vacuum pinholder_commit vacuumer_nonaggressive_vacuum
+starting permutation: dml_begin dml_other_begin dml_key_share dml_other_key_share vacuumer_vacuum_noprune pinholder_cursor dml_other_update dml_commit dml_other_commit vacuumer_vacuum_noprune pinholder_commit vacuumer_vacuum_noprune
 step dml_begin: BEGIN;
 step dml_other_begin: BEGIN;
 step dml_key_share: SELECT id FROM smalltbl WHERE id = 3 FOR KEY SHARE;
@@ -162,7 +162,7 @@ id
  3
 (1 row)
 
-step vacuumer_nonaggressive_vacuum: 
+step vacuumer_vacuum_noprune: 
   VACUUM smalltbl;
 
 step pinholder_cursor: 
@@ -178,12 +178,12 @@ dummy
 step dml_other_update: UPDATE smalltbl SET t = 'u' WHERE id = 3;
 step dml_commit: COMMIT;
 step dml_other_commit: COMMIT;
-step vacuumer_nonaggressive_vacuum: 
+step vacuumer_vacuum_noprune: 
   VACUUM smalltbl;
 
 step pinholder_commit: 
   COMMIT;
 
-step vacuumer_nonaggressive_vacuum: 
+step vacuumer_vacuum_noprune: 
   VACUUM smalltbl;
 
diff --git a/src/test/isolation/specs/vacuum-no-cleanup-lock.spec b/src/test/isolation/specs/vacuum-no-cleanup-lock.spec
index 05fd280f6..927410258 100644
--- a/src/test/isolation/specs/vacuum-no-cleanup-lock.spec
+++ b/src/test/isolation/specs/vacuum-no-cleanup-lock.spec
@@ -55,15 +55,18 @@ step dml_other_key_share  { SELECT id FROM smalltbl WHERE id = 3 FOR KEY SHARE;
 step dml_other_update     { UPDATE smalltbl SET t = 'u' WHERE id = 3; }
 step dml_other_commit     { COMMIT; }
 
-# This session runs non-aggressive VACUUM, but with maximally aggressive
-# cutoffs for tuple freezing (e.g., FreezeLimit == OldestXmin):
+# This session runs VACUUM with maximally aggressive cutoffs for tuple
+# freezing (e.g., FreezeLimit == OldestXmin), without ever being
+# prepared to wait for a cleanup lock (we'll never wait on a cleanup
+# lock because the separate MinXid cutoff for waiting will still be
+# well before FreezeLimit, given our default autovacuum_freeze_max_age).
 session vacuumer
 setup
 {
   SET vacuum_freeze_min_age = 0;
   SET vacuum_multixact_freeze_min_age = 0;
 }
-step vacuumer_nonaggressive_vacuum
+step vacuumer_vacuum_noprune
 {
   VACUUM smalltbl;
 }
@@ -75,15 +78,14 @@ step vacuumer_pg_class_stats
 # Test VACUUM's reltuples counting mechanism.
 #
 # Final pg_class.reltuples should never be affected by VACUUM's inability to
-# get a cleanup lock on any page, except to the extent that any cleanup lock
-# contention changes the number of tuples that remain ("missed dead" tuples
-# are counted in reltuples, much like "recently dead" tuples).
+# get a cleanup lock on any page.  Note that "missed dead" tuples are counted
+# in reltuples, much like "recently dead" tuples.
 
 # Easy case:
 permutation
     vacuumer_pg_class_stats  # Start with 20 tuples
     dml_insert
-    vacuumer_nonaggressive_vacuum
+    vacuumer_vacuum_noprune
     vacuumer_pg_class_stats  # End with 21 tuples
 
 # Harder case -- count 21 tuples at the end (like last time), but with cleanup
@@ -92,7 +94,7 @@ permutation
     vacuumer_pg_class_stats  # Start with 20 tuples
     dml_insert
     pinholder_cursor
-    vacuumer_nonaggressive_vacuum
+    vacuumer_vacuum_noprune
     vacuumer_pg_class_stats  # End with 21 tuples
     pinholder_commit  # order doesn't matter
 
@@ -103,7 +105,7 @@ permutation
     dml_insert
     dml_delete
     dml_insert
-    vacuumer_nonaggressive_vacuum
+    vacuumer_vacuum_noprune
     # reltuples is 21 here again -- "recently dead" tuple won't be included in
     # count here:
     vacuumer_pg_class_stats
@@ -116,7 +118,7 @@ permutation
     dml_delete
     pinholder_cursor
     dml_insert
-    vacuumer_nonaggressive_vacuum
+    vacuumer_vacuum_noprune
     # reltuples is 21 here again -- "missed dead" tuple ("recently dead" when
     # concurrent activity held back VACUUM's OldestXmin) won't be included in
     # count here:
@@ -128,7 +130,7 @@ permutation
 # This provides test coverage for code paths that are only hit when we need to
 # freeze, but inability to acquire a cleanup lock on a heap page makes
 # freezing some XIDs/MXIDs < FreezeLimit/MultiXactCutoff impossible (without
-# waiting for a cleanup lock, which non-aggressive VACUUM is unwilling to do).
+# waiting for a cleanup lock, which won't ever happen here).
 permutation
     dml_begin
     dml_other_begin
@@ -136,15 +138,15 @@ permutation
     dml_other_key_share
     # Will get cleanup lock, can't advance relminmxid yet:
     # (though will usually advance relfrozenxid by ~2 XIDs)
-    vacuumer_nonaggressive_vacuum
+    vacuumer_vacuum_noprune
     pinholder_cursor
     dml_other_update
     dml_commit
     dml_other_commit
     # Can't cleanup lock, so still can't advance relminmxid here:
     # (relfrozenxid held back by XIDs in MultiXact too)
-    vacuumer_nonaggressive_vacuum
+    vacuumer_vacuum_noprune
     pinholder_commit
     # Pin was dropped, so will advance relminmxid, at long last:
     # (ditto for relfrozenxid advancement)
-    vacuumer_nonaggressive_vacuum
+    vacuumer_vacuum_noprune
diff --git a/src/test/regress/expected/reloptions.out b/src/test/regress/expected/reloptions.out
index b6aef6f65..0e569d300 100644
--- a/src/test/regress/expected/reloptions.out
+++ b/src/test/regress/expected/reloptions.out
@@ -102,8 +102,8 @@ SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
 INSERT INTO reloptions_test VALUES (1, NULL), (NULL, NULL);
 ERROR:  null value in column "i" of relation "reloptions_test" violates not-null constraint
 DETAIL:  Failing row contains (null, null).
--- Do an aggressive vacuum to prevent page-skipping.
-VACUUM (FREEZE, DISABLE_PAGE_SKIPPING) reloptions_test;
+-- Do a VACUUM FREEZE to prevent skipping any pruning.
+VACUUM FREEZE reloptions_test;
 SELECT pg_relation_size('reloptions_test') > 0;
  ?column? 
 ----------
@@ -128,8 +128,8 @@ SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
 INSERT INTO reloptions_test VALUES (1, NULL), (NULL, NULL);
 ERROR:  null value in column "i" of relation "reloptions_test" violates not-null constraint
 DETAIL:  Failing row contains (null, null).
--- Do an aggressive vacuum to prevent page-skipping.
-VACUUM (FREEZE, DISABLE_PAGE_SKIPPING) reloptions_test;
+-- Do a VACUUM FREEZE to prevent skipping any pruning.
+VACUUM FREEZE reloptions_test;
 SELECT pg_relation_size('reloptions_test') = 0;
  ?column? 
 ----------
diff --git a/src/test/regress/sql/reloptions.sql b/src/test/regress/sql/reloptions.sql
index 4252b0202..b2bed8ed8 100644
--- a/src/test/regress/sql/reloptions.sql
+++ b/src/test/regress/sql/reloptions.sql
@@ -61,8 +61,8 @@ CREATE TEMP TABLE reloptions_test(i INT NOT NULL, j text)
 	autovacuum_enabled=false);
 SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
 INSERT INTO reloptions_test VALUES (1, NULL), (NULL, NULL);
--- Do an aggressive vacuum to prevent page-skipping.
-VACUUM (FREEZE, DISABLE_PAGE_SKIPPING) reloptions_test;
+-- Do a VACUUM FREEZE to prevent skipping any pruning.
+VACUUM FREEZE reloptions_test;
 SELECT pg_relation_size('reloptions_test') > 0;
 
 SELECT reloptions FROM pg_class WHERE oid =
@@ -72,8 +72,8 @@ SELECT reloptions FROM pg_class WHERE oid =
 ALTER TABLE reloptions_test RESET (vacuum_truncate);
 SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass;
 INSERT INTO reloptions_test VALUES (1, NULL), (NULL, NULL);
--- Do an aggressive vacuum to prevent page-skipping.
-VACUUM (FREEZE, DISABLE_PAGE_SKIPPING) reloptions_test;
+-- Do a VACUUM FREEZE to prevent skipping any pruning.
+VACUUM FREEZE reloptions_test;
 SELECT pg_relation_size('reloptions_test') = 0;
 
 -- Test toast.* options
-- 
2.38.1

