From 5071ca4e60888fffbf4e2ef526f102da41c7cab7 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Mon, 18 Jul 2022 15:13:27 -0700
Subject: [PATCH v3 3/5] Add eager freezing strategy to VACUUM.

Avoid large build-ups of all-visible pages by making non-aggressive
VACUUMs freeze pages proactively for VACUUMs/tables where eager
vacuuming is deemed appropriate.  Use of the eager strategy (an
alternative to the classic lazy freezing strategy) is controlled by a
new GUC, vacuum_freeze_strategy_threshold (and an associated
autovacuum_* reloption).  Tables whose rel_pages are >= the cutoff will
have VACUUM use the eager freezing strategy.  Otherwise we use the lazy
freezing strategy, which is the classic approach (actually, we always
use eager freezing in aggressive VACUUMs, though they are expected to be
much rarer now).

When the eager strategy is in use, lazy_scan_prune will trigger freezing
a page's tuples at the point that it notices that it will at least
become all-visible -- it can be made all-frozen instead.  We still
respect FreezeLimit, though: the presence of any XID < FreezeLimit also
triggers page-level freezing (just as it would with the lazy strategy).

If and when a smaller table (a table that uses lazy freezing at first)
grows past the table size threshold, the next VACUUM against the table
shouldn't have to do too much extra freezing to catch up when we perform
eager freezing for the first time (the table still won't be very large).
Once VACUUM has caught up, the amount of work required in each VACUUM
operation should be roughly proportionate to the number of new pages, at
least with a pure append-only table.

In summary, we try to get the benefit of the lazy freezing strategy,
without ever allowing VACUUM to fall uncomfortably far behind.  In
particular, we avoid accumulating an excessive number of unfrozen
all-visible pages in any one table.  This approach is often enough to
keep relfrozenxid recent, but we still have aggressive/antiwraparound
autovacuums for tables where it doesn't work out that way.

Note that freezing strategy is distinct from (though related to) the
strategy for skipping pages with the visibility map.  In practice tables
that use eager freezing always eagerly scan all-visible pages (they
prioritize advancing relfrozenxid), partly because we expect few or no
all-visible pages there (at least during the second or subsequent VACUUM
that uses eager freezing).  When VACUUM uses the classic/lazy freezing
strategy, VACUUM will also scan pages eagerly (i.e. it will scan any
all-visible pages and only skip all-frozen pages) when the added cost is
relatively low.
---
 src/include/access/heapam_xlog.h              |  8 +-
 src/include/commands/vacuum.h                 |  4 +
 src/include/utils/rel.h                       |  1 +
 src/backend/access/common/reloptions.c        | 11 +++
 src/backend/access/heap/heapam.c              |  8 +-
 src/backend/access/heap/vacuumlazy.c          | 76 ++++++++++++++++---
 src/backend/commands/vacuum.c                 |  4 +
 src/backend/postmaster/autovacuum.c           | 10 +++
 src/backend/utils/misc/guc.c                  | 11 +++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 doc/src/sgml/config.sgml                      | 31 ++++++--
 doc/src/sgml/maintenance.sgml                 |  6 +-
 doc/src/sgml/ref/create_table.sgml            | 14 ++++
 13 files changed, 162 insertions(+), 23 deletions(-)

diff --git a/src/include/access/heapam_xlog.h b/src/include/access/heapam_xlog.h
index 40556271d..9ea1db505 100644
--- a/src/include/access/heapam_xlog.h
+++ b/src/include/access/heapam_xlog.h
@@ -345,7 +345,11 @@ typedef struct xl_heap_freeze_tuple
  * pg_class tuple.
  *
  * Alternative "no freeze" variants of relfrozenxid_nofreeze_out and
- * relminmxid_nofreeze_out must also be maintained for !freeze pages.
+ * relminmxid_nofreeze_out must also be maintained.  If vacuumlazy.c caller
+ * opts to not execute freeze plans produced by heap_prepare_freeze_tuple for
+ * its own reasons, then new relfrozenxid and relminmxid values must reflect
+ * that that choice was made.  (This is only safe when 'freeze' is still unset
+ * after the final last heap_prepare_freeze_tuple call for the page.)
  */
 typedef struct page_frozenxid_tracker
 {
@@ -356,7 +360,7 @@ typedef struct page_frozenxid_tracker
 	TransactionId relfrozenxid_out;
 	MultiXactId relminmxid_out;
 
-	/* Used by caller for '!freeze' pages */
+	/* Used by caller that opts not to freeze a '!freeze' page */
 	TransactionId relfrozenxid_nofreeze_out;
 	MultiXactId relminmxid_nofreeze_out;
 
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 5d816ba7f..52379f819 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -220,6 +220,9 @@ typedef struct VacuumParams
 											 * use default */
 	int			multixact_freeze_table_age; /* multixact age at which to scan
 											 * whole table */
+	int			freeze_strategy_threshold;	/* threshold to use eager
+											 * freezing, in total heap blocks,
+											 * -1 to use default */
 	bool		is_wraparound;	/* force a for-wraparound vacuum */
 	int			log_min_duration;	/* minimum execution threshold in ms at
 									 * which autovacuum is logged, -1 to use
@@ -256,6 +259,7 @@ extern PGDLLIMPORT int vacuum_freeze_min_age;
 extern PGDLLIMPORT int vacuum_freeze_table_age;
 extern PGDLLIMPORT int vacuum_multixact_freeze_min_age;
 extern PGDLLIMPORT int vacuum_multixact_freeze_table_age;
+extern PGDLLIMPORT int vacuum_freeze_strategy_threshold;
 extern PGDLLIMPORT int vacuum_failsafe_age;
 extern PGDLLIMPORT int vacuum_multixact_failsafe_age;
 
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index 7dc401cf0..c6d8265cf 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -308,6 +308,7 @@ typedef struct AutoVacOpts
 	int			vacuum_ins_threshold;
 	int			analyze_threshold;
 	int			vacuum_cost_limit;
+	int			freeze_strategy_threshold;
 	int			freeze_min_age;
 	int			freeze_max_age;
 	int			freeze_table_age;
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 609329bb2..f4e2109e7 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -260,6 +260,15 @@ static relopt_int intRelOpts[] =
 		},
 		-1, 1, 10000
 	},
+	{
+		{
+			"autovacuum_freeze_strategy_threshold",
+			"Table size at which VACUUM freezes using eager strategy.",
+			RELOPT_KIND_HEAP | RELOPT_KIND_TOAST,
+			ShareUpdateExclusiveLock
+		},
+		-1, 0, INT_MAX
+	},
 	{
 		{
 			"autovacuum_freeze_min_age",
@@ -1851,6 +1860,8 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
 		offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_threshold)},
 		{"autovacuum_vacuum_cost_limit", RELOPT_TYPE_INT,
 		offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_limit)},
+		{"autovacuum_freeze_strategy_threshold", RELOPT_TYPE_INT,
+		offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_strategy_threshold)},
 		{"autovacuum_freeze_min_age", RELOPT_TYPE_INT,
 		offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_min_age)},
 		{"autovacuum_freeze_max_age", RELOPT_TYPE_INT,
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index d6aea370f..699a5acae 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -6429,7 +6429,13 @@ FreezeMultiXactId(MultiXactId multi, uint16 t_infomask,
  * WAL-log what we would need to do, and return true.  Return false if nothing
  * is to be changed.  In addition, set *totally_frozen to true if the tuple
  * will be totally frozen after these operations are performed and false if
- * more freezing will eventually be required.
+ * more freezing will eventually be required (assuming page is to be frozen).
+ *
+ * Although this interface is primarily tuple-based, caller decides on whether
+ * or not to freeze the page as a whole.  We'll often help caller to prepare a
+ * complete "freeze plan" that it ultimately discards.  However, our caller
+ * doesn't always get to choose; it must freeze when xtrack.freeze is set
+ * here.  This ensures that any XIDs < limit_xid are never left behind.
  *
  * Caller must initialize xtrack fields for page as a whole before calling
  * here with first tuple for the page.  See page_frozenxid_tracker comments.
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 97e272081..8d750ac7f 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -110,7 +110,7 @@
 
 /*
  * Threshold that controls whether non-aggressive VACUUMs will skip any
- * all-visible pages
+ * all-visible pages when using the lazy freezing strategy
  */
 #define SKIPALLVIS_THRESHOLD_PAGES	0.05	/* i.e. 5% of rel_pages */
 
@@ -150,6 +150,8 @@ typedef struct LVRelState
 	bool		skipallvis;
 	/* Skip (don't scan) all-frozen pages? */
 	bool		skipallfrozen;
+	/* Proactively freeze all tuples on pages about to be set all-visible? */
+	bool		allvis_freeze_strategy;
 	/* Wraparound failsafe has been triggered? */
 	bool		failsafe_active;
 	/* Consider index vacuuming bypass optimization? */
@@ -254,6 +256,7 @@ typedef struct LVSavedErrInfo
 /* non-export function prototypes */
 static void lazy_scan_heap(LVRelState *vacrel);
 static BlockNumber lazy_scan_strategy(LVRelState *vacrel,
+									  BlockNumber eager_threshold,
 									  BlockNumber all_visible,
 									  BlockNumber all_frozen);
 static BlockNumber lazy_scan_skip(LVRelState *vacrel,
@@ -328,6 +331,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	MultiXactId OldestMxact,
 				MultiXactCutoff;
 	BlockNumber orig_rel_pages,
+				eager_threshold,
 				all_visible,
 				all_frozen,
 				scanned_pages,
@@ -367,6 +371,10 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	 * used to determine which XIDs/MultiXactIds will be frozen.  If this is
 	 * an aggressive VACUUM then lazy_scan_heap cannot leave behind unfrozen
 	 * XIDs < FreezeLimit (all MXIDs < MultiXactCutoff also need to go away).
+	 *
+	 * Also determine our cutoff for applying the eager/all-visible freezing
+	 * strategy.  If rel_pages is larger than this cutoff we use the strategy,
+	 * even during non-aggressive VACUUMs.
 	 */
 	aggressive = vacuum_set_xid_limits(rel,
 									   params->freeze_min_age,
@@ -375,6 +383,9 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 									   params->multixact_freeze_table_age,
 									   &OldestXmin, &OldestMxact,
 									   &FreezeLimit, &MultiXactCutoff);
+	eager_threshold = params->freeze_strategy_threshold < 0 ?
+		vacuum_freeze_strategy_threshold :
+		params->freeze_strategy_threshold;
 
 	skipallfrozen = true;
 	if (params->options & VACOPT_DISABLE_PAGE_SKIPPING)
@@ -529,7 +540,7 @@ 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,
+	scanned_pages = lazy_scan_strategy(vacrel, eager_threshold,
 									   all_visible, all_frozen);
 	if (verbose)
 	{
@@ -1304,17 +1315,28 @@ lazy_scan_heap(LVRelState *vacrel)
 }
 
 /*
- *	lazy_scan_strategy() -- Determine skipping strategy.
+ *	lazy_scan_strategy() -- Determine freezing/skipping strategy.
  *
- * Determines if the ongoing VACUUM operation should skip all-visible pages
- * for non-aggressive VACUUMs, where advancing relfrozenxid is optional.
+ * Our traditional/lazy freezing strategy is useful when putting off the work
+ * of freezing totally avoids work that turns out to have been unnecessary.
+ * On the other hand we eagerly freeze pages when that strategy spreads out
+ * the burden of freezing over time.  Performance stability is important; no
+ * one VACUUM operation should need to freeze disproportionately many pages.
+ * Antiwraparound VACUUMs of append-only tables should generally be avoided.
+ *
+ * Also determines if the ongoing VACUUM operation should skip all-visible
+ * pages for non-aggressive VACUUMs, where 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.
  *
  * 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, BlockNumber eager_threshold,
+				   BlockNumber all_visible, BlockNumber all_frozen)
 {
 	BlockNumber rel_pages = vacrel->rel_pages,
 				scanned_pages_skipallvis,
@@ -1347,21 +1369,48 @@ lazy_scan_strategy(LVRelState *vacrel,
 		scanned_pages_skipallfrozen++;
 
 	/*
-	 * Okay, now we have all the information we need to decide on a strategy
+	 * Okay, now we have all the information we need to decide on a strategy.
+	 *
+	 * We use the all-visible/eager freezing strategy when a threshold
+	 * controlled by the freeze_strategy_threshold GUC/reloption is crossed.
+	 * VACUUM won't accumulate any unfrozen all-visible pages over time in
+	 * tables above the threshold.  The system won't fall behind on freezing.
 	 */
 	if (!vacrel->skipallfrozen)
 	{
 		/* DISABLE_PAGE_SKIPPING makes all skipping unsafe */
 		Assert(vacrel->aggressive && !vacrel->skipallvis);
+		vacrel->allvis_freeze_strategy = true;
 		return rel_pages;
 	}
 	else if (vacrel->aggressive)
+	{
+		/* Always freeze all-visible pages during aggressive VACUUMs */
 		Assert(!vacrel->skipallvis);
+		vacrel->allvis_freeze_strategy = true;
+	}
+	else if (rel_pages >= eager_threshold)
+	{
+		/*
+		 * Non-aggressive VACUUM of table whose rel_pages now exceeds
+		 * GUC-based threshold for eager freezing.
+		 *
+		 * We always scan all-visible pages when the threshold is crossed, so
+		 * that relfrozenxid can be advanced.  There will typically be few or
+		 * no all-visible pages (only all-frozen) in the table anyway, at
+		 * least after the first VACUUM that exceeds the threshold.
+		 */
+		vacrel->allvis_freeze_strategy = true;
+		vacrel->skipallvis = false;
+	}
 	else
 	{
 		BlockNumber nextra,
 					nextra_threshold;
 
+		/* Non-aggressive VACUUM of small table -- use lazy freeze strategy */
+		vacrel->allvis_freeze_strategy = false;
+
 		/*
 		 * Decide on whether or not we'll skip all-visible pages.
 		 *
@@ -1873,8 +1922,15 @@ retry:
 	 *
 	 * Freeze the page when heap_prepare_freeze_tuple indicates that at least
 	 * one XID/MXID from before FreezeLimit/MultiXactCutoff is present.
+	 *
+	 * When ongoing VACUUM opted to use the all-visible freezing strategy we
+	 * freeze any page that will become all-visible, making it all-frozen
+	 * instead. (Actually, there are edge-cases where this might not result in
+	 * marking the page all-frozen in the visibility map, but that should have
+	 * only a negligible impact.)
 	 */
-	if (xtrack.freeze || tuples_frozen == 0)
+	if (xtrack.freeze || tuples_frozen == 0 ||
+		(vacrel->allvis_freeze_strategy && prunestate->all_visible))
 	{
 		/*
 		 * We're freezing the page.  Our final NewRelfrozenXid doesn't need to
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 7ccde07de..b837e0331 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -67,6 +67,7 @@ int			vacuum_freeze_min_age;
 int			vacuum_freeze_table_age;
 int			vacuum_multixact_freeze_min_age;
 int			vacuum_multixact_freeze_table_age;
+int			vacuum_freeze_strategy_threshold;
 int			vacuum_failsafe_age;
 int			vacuum_multixact_failsafe_age;
 
@@ -263,6 +264,9 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
 		params.multixact_freeze_table_age = -1;
 	}
 
+	/* Determine freezing strategy later on using GUC or reloption */
+	params.freeze_strategy_threshold = -1;
+
 	/* user-invoked vacuum is never "for wraparound" */
 	params.is_wraparound = false;
 
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 9dc6bf947..18a8e8b80 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -150,6 +150,7 @@ static int	default_freeze_min_age;
 static int	default_freeze_table_age;
 static int	default_multixact_freeze_min_age;
 static int	default_multixact_freeze_table_age;
+static int	default_freeze_strategy_threshold;
 
 /* Memory context for long-lived data */
 static MemoryContext AutovacMemCxt;
@@ -2009,6 +2010,7 @@ do_autovacuum(void)
 		default_freeze_table_age = 0;
 		default_multixact_freeze_min_age = 0;
 		default_multixact_freeze_table_age = 0;
+		default_freeze_strategy_threshold = 0;
 	}
 	else
 	{
@@ -2016,6 +2018,7 @@ do_autovacuum(void)
 		default_freeze_table_age = vacuum_freeze_table_age;
 		default_multixact_freeze_min_age = vacuum_multixact_freeze_min_age;
 		default_multixact_freeze_table_age = vacuum_multixact_freeze_table_age;
+		default_freeze_strategy_threshold = vacuum_freeze_strategy_threshold;
 	}
 
 	ReleaseSysCache(tuple);
@@ -2800,6 +2803,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
 		int			freeze_table_age;
 		int			multixact_freeze_min_age;
 		int			multixact_freeze_table_age;
+		int			freeze_strategy_threshold;
 		int			vac_cost_limit;
 		double		vac_cost_delay;
 		int			log_min_duration;
@@ -2849,6 +2853,11 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
 			? avopts->multixact_freeze_table_age
 			: default_multixact_freeze_table_age;
 
+		freeze_strategy_threshold = (avopts &&
+									 avopts->freeze_strategy_threshold >= 0)
+			? avopts->freeze_strategy_threshold
+			: default_freeze_strategy_threshold;
+
 		tab = palloc(sizeof(autovac_table));
 		tab->at_relid = relid;
 		tab->at_sharedrel = classForm->relisshared;
@@ -2871,6 +2880,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
 		tab->at_params.freeze_table_age = freeze_table_age;
 		tab->at_params.multixact_freeze_min_age = multixact_freeze_min_age;
 		tab->at_params.multixact_freeze_table_age = multixact_freeze_table_age;
+		tab->at_params.freeze_strategy_threshold = freeze_strategy_threshold;
 		tab->at_params.is_wraparound = wraparound;
 		tab->at_params.log_min_duration = log_min_duration;
 		tab->at_vacuum_cost_limit = vac_cost_limit;
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 55bf99851..bb018ae62 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -2733,6 +2733,17 @@ static struct config_int ConfigureNamesInt[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"vacuum_freeze_strategy_threshold", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Table size at which VACUUM freezes using eager strategy."),
+			NULL,
+			GUC_UNIT_BLOCKS
+		},
+		&vacuum_freeze_strategy_threshold,
+		(UINT64CONST(4) * 1024 * 1024 * 1024) / BLCKSZ, 0, INT_MAX,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"vacuum_defer_cleanup_age", PGC_SIGHUP, REPLICATION_PRIMARY,
 			gettext_noop("Number of transactions by which VACUUM and HOT cleanup should be deferred, if any."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 90bec0502..e701e464e 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -694,6 +694,7 @@
 #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
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index a5cd4e44c..091be17c3 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -9147,6 +9147,21 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </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>
+       <primary><varname>vacuum_freeze_strategy_threshold</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Specifies the cutoff size (in pages) that <command>VACUUM</command>
+        should use to decide whether to its eager freezing strategy.
+        The default is 4 gigabytes (<literal>4GB</literal>).
+       </para>
+      </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>
@@ -9155,9 +9170,11 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </term>
       <listitem>
        <para>
-        Specifies the cutoff age (in transactions) that <command>VACUUM</command>
-        should use to decide whether to freeze row versions
-        while scanning a table.
+        Specifies the cutoff age (in transactions) that
+        <command>VACUUM</command> should use to decide whether to
+        trigger freezing of pages that have an older XID.  When VACUUM
+        uses its eager freezing strategy, freezing a page can also be
+        triggered when the page contains only all-visible tuples.
         The default is 50 million transactions.  Although
         users can set this value anywhere from zero to one billion,
         <command>VACUUM</command> will silently limit the effective value to half
@@ -9234,10 +9251,10 @@ 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 replace multixact IDs with a newer
-        transaction ID or multixact ID while scanning a table.  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/maintenance.sgml b/doc/src/sgml/maintenance.sgml
index 759ea5ac9..554b3a75d 100644
--- a/doc/src/sgml/maintenance.sgml
+++ b/doc/src/sgml/maintenance.sgml
@@ -588,9 +588,9 @@
     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> (typically the most recent
-    aggressive VACUUM).  Similarly, the
-    <structfield>datfrozenxid</structfield> column of a database's
+    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.
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index c14b2010d..7e684d187 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1680,6 +1680,20 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
+   <varlistentry id="reloption-autovacuum-freeze-strategy-threshold" xreflabel="autovacuum_freeze_strategy_threshold">
+    <term><literal>autovacuum_freeze_strategy_threshold</literal>, <literal>toast.autovacuum_freeze_strategy_threshold</literal> (<type>integer</type>)
+    <indexterm>
+     <primary><varname>autovacuum_freeze_strategy_threshold</varname> storage parameter</primary>
+    </indexterm>
+    </term>
+    <listitem>
+     <para>
+      Per-table value for <xref linkend="guc-vacuum-freeze-strategy-threshold"/>
+      parameter.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry id="reloption-autovacuum-freeze-min-age" xreflabel="autovacuum_freeze_min_age">
     <term><literal>autovacuum_freeze_min_age</literal>, <literal>toast.autovacuum_freeze_min_age</literal> (<type>integer</type>)
     <indexterm>
-- 
2.34.1

