From ad713713814b380bd20cd0ce8e0a33a3b4fa572d Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Mon, 18 Jul 2022 15:13:27 -0700
Subject: [PATCH v2 3/4] 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          | 80 ++++++++++++++++---
 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                      | 15 ++++
 doc/src/sgml/maintenance.sgml                 |  6 +-
 doc/src/sgml/ref/create_table.sgml            | 14 ++++
 13 files changed, 155 insertions(+), 18 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 9ba975c1a..ba54e5767 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? */
@@ -252,6 +254,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,
@@ -327,6 +330,7 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	MultiXactId OldestMxact,
 				MultiXactCutoff;
 	BlockNumber orig_rel_pages,
+				eager_threshold,
 				all_visible,
 				all_frozen,
 				scanned_pages,
@@ -366,6 +370,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,
@@ -374,6 +382,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)
@@ -523,10 +534,10 @@ heap_vacuum_rel(Relation rel, VacuumParams *params,
 	vacrel->NewRelminMxid = OldestMxact;
 
 	/*
-	 * Use visibility map snapshot to determine whether we'll skip all-visible
-	 * pages using vmsnap in lazy_scan_heap
+	 * Use visibility map snapshot to determine freezing strategy, and whether
+	 * we'll skip all-visible pages using vmsnap in lazy_scan_heap
 	 */
-	scanned_pages = lazy_scan_strategy(vacrel,
+	scanned_pages = lazy_scan_strategy(vacrel, eager_threshold,
 									   all_visible, all_frozen);
 	if (verbose)
 	{
@@ -1307,17 +1318,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,
@@ -1350,21 +1372,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 treshold 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.
 		 *
@@ -1882,8 +1931,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 || nfrozen == 0)
+	if (xtrack.freeze || nfrozen == 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 b3b1afba8..ff78152b4 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;
@@ -2006,6 +2007,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
 	{
@@ -2013,6 +2015,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);
@@ -2797,6 +2800,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;
@@ -2846,6 +2850,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;
@@ -2868,6 +2877,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 9fbbfb1be..06b1bf764 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -2736,6 +2736,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..ba3e012a0 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>
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

