From 27edfce6bc6414e79d79741248cfe5eae258e92a Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Sat, 22 Apr 2023 13:04:13 -0700
Subject: [PATCH v3 9/9] Overhaul freezing and wraparound docs.

This is almost a complete rewrite.  "Preventing Transaction ID
Wraparound Failures" becomes "Freezing to manage the transaction ID
space".  This is follow-up work to commit 1de58df4, which added
page-level freezing to VACUUM.

The emphasis is now on the physical work of freezing pages.  This flows
a little better than it otherwise would due to recent structural
cleanups to maintenance.sgml; discussion about freezing now immediately
follows discussion of cleanup of dead tuples.  We still talk about the
problem of the system activating xidStopLimit protections in the same
section, but we use much less alarmist language about data corruption,
and are no longer overly concerned about the very worst case.  We don't
rescind the recommendation that users recover from an xidStopLimit
outage by using single user mode, though that seems like something we
should aim to do in the near future.

There is no longer a separate sect3 to discuss MultiXactId related
issues.  VACUUM now performs exactly the same processing steps when it
freezes a page, independent of the trigger condition.

Also describe the page-level freezing FPI optimization added by commit
1de58df4.  This is expected to trigger the majority of all freezing with
many types of workloads.
---
 doc/src/sgml/config.sgml                  |  20 +-
 doc/src/sgml/logicaldecoding.sgml         |   2 +-
 doc/src/sgml/maintenance.sgml             | 967 ++++++++++++++++------
 doc/src/sgml/ref/create_table.sgml        |   2 +-
 doc/src/sgml/ref/prepare_transaction.sgml |   2 +-
 doc/src/sgml/ref/vacuum.sgml              |   6 +-
 doc/src/sgml/ref/vacuumdb.sgml            |   4 +-
 doc/src/sgml/xact.sgml                    |   2 +-
 8 files changed, 724 insertions(+), 281 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index b56f073a9..a4ac4e740 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8359,7 +8359,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="freezing-xid-space"/> for more information.
        </para>
       </listitem>
      </varlistentry>
@@ -8548,7 +8548,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="freezing-xid-space"/>.
        </para>
       </listitem>
      </varlistentry>
@@ -8577,7 +8577,7 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         400 million multixacts.
         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-multixact-wraparound"/>.
+        For more information see <xref linkend="anti-wraparound-autovacuums"/>.
        </para>
       </listitem>
      </varlistentry>
@@ -9284,7 +9284,7 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         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"/>.
+        <xref linkend="aggressive-vacuum"/>.
        </para>
       </listitem>
      </varlistentry>
@@ -9306,7 +9306,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="freezing-xid-space"/>.
        </para>
       </listitem>
      </varlistentry>
@@ -9343,7 +9343,8 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         set this value anywhere from zero to 2.1 billion,
         <command>VACUUM</command> will silently adjust the effective
         value to no less than 105% of <xref
-         linkend="guc-autovacuum-freeze-max-age"/>.
+         linkend="guc-autovacuum-freeze-max-age"/>.  For more
+        information see <xref linkend="xid-stop-limit"/>.
        </para>
       </listitem>
      </varlistentry>
@@ -9367,7 +9368,7 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         <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"/>.
+        For more information see <xref linkend="anti-wraparound-autovacuums"/>.
        </para>
       </listitem>
      </varlistentry>
@@ -9388,7 +9389,7 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         the value of <xref linkend="guc-autovacuum-multixact-freeze-max-age"/>,
         so that there is not an unreasonably short time between forced
         autovacuums.
-        For more information see <xref linkend="vacuum-for-multixact-wraparound"/>.
+        For more information see <xref linkend="freezing-xid-space"/>.
        </para>
       </listitem>
      </varlistentry>
@@ -9421,7 +9422,8 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
         this value anywhere from zero to 2.1 billion,
         <command>VACUUM</command> will silently adjust the effective
         value to no less than 105% of <xref
-         linkend="guc-autovacuum-multixact-freeze-max-age"/>.
+         linkend="guc-autovacuum-multixact-freeze-max-age"/>.  For more
+        information see <xref linkend="xid-stop-limit"/>.
        </para>
       </listitem>
      </varlistentry>
diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml
index cbd3aa804..80dade3be 100644
--- a/doc/src/sgml/logicaldecoding.sgml
+++ b/doc/src/sgml/logicaldecoding.sgml
@@ -353,7 +353,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="freezing-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 5546d8c7d..a480e4f8e 100644
--- a/doc/src/sgml/maintenance.sgml
+++ b/doc/src/sgml/maintenance.sgml
@@ -148,13 +148,8 @@ vacuum insert threshold = vacuum base insert threshold + vacuum insert scale fac
    <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;
+   can reduce the work required in subsequent 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
@@ -273,15 +268,20 @@ analyze threshold = analyze base threshold + analyze scale factor * number of tu
     </listitem>
 
     <listitem>
-     <simpara>To protect against loss of very old data due to
-     <firstterm>transaction ID wraparound</firstterm> or
-     <firstterm>multixact ID wraparound</firstterm>.</simpara>
+     <simpara>To maintain the system's ability to allocated new
+     transaction IDs through freezing.</simpara>
     </listitem>
 
     <listitem>
      <simpara>To update the visibility map, which speeds
      up <link linkend="indexes-index-only-scans">index-only
-     scans</link>.</simpara>
+     scans</link>, and helps the next <command>VACUUM</command>
+     operation avoid needlessly scanning already-frozen pages.</simpara>
+    </listitem>
+
+    <listitem>
+     <simpara>To truncate obsolescent transaction status information,
+     when possible.</simpara>
     </listitem>
 
     <listitem>
@@ -483,302 +483,671 @@ analyze threshold = analyze base threshold + analyze scale factor * number of tu
    </warning>
   </sect2>
 
-  <sect2 id="vacuum-for-wraparound">
-   <title>Preventing Transaction ID Wraparound Failures</title>
-
-   <indexterm zone="vacuum-for-wraparound">
-    <primary>transaction ID</primary>
-    <secondary>wraparound</secondary>
-   </indexterm>
+  <sect2 id="freezing-xid-space">
+   <title>Freezing to manage the transaction ID space</title>
 
    <indexterm>
-    <primary>wraparound</primary>
-    <secondary>of transaction IDs</secondary>
+    <primary>Freezing</primary>
+    <secondary>of transaction IDs and MultiXact IDs</secondary>
    </indexterm>
 
    <para>
-    <productname>PostgreSQL</productname>'s <link
-     linkend="mvcc-intro">MVCC</link> transaction semantics depend on
-    being able to compare <glossterm linkend="glossary-xid">transaction
-     ID numbers (<acronym>XID</acronym>)</glossterm> to determine
-    whether or not the row is visible to each query's MVCC snapshot
-    (see <xref linkend="interpreting-xid-stamps"/>).  But since
-    on-disk storage of transaction IDs in heap pages uses a truncated
-    32-bit representation to save space (rather than the full 64-bit
-    representation), it is necessary to vacuum every table in every
-    database <emphasis>at least</emphasis> once every two billion
-    transactions (though far more frequent vacuuming is typical).
+    <command>VACUUM</command> often marks some of the pages that it
+    scans <emphasis>frozen</emphasis>, indicating that all eligible
+    rows on the page 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.  The specific Transaction ID number
+    (<acronym>XID</acronym>) stored in a frozen heap row's
+    <structfield>xmin</structfield> field is no longer needed to
+    determine anything about the row's visibility.  Furthermore, when
+    a row undergoing freezing happens to have an XID set in its
+    <structfield>xmax</structfield> field (possibly an XID left behind
+    by an earlier <command>SELECT FOR UPDATE</command> row locker),
+    the <structfield>xmax</structfield> field's XID is usually also
+    removed.
    </para>
 
    <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.
+    Once frozen, heap pages are <quote>self-contained</quote>.  Every
+    query can read all of the page's rows in a way that assumes that
+    the inserting transaction committed and is visible to its
+    <acronym>MVCC</acronym> snapshot.  No query will ever have to
+    consult external transaction status metadata to interpret the
+    page's contents, either.  In particular,
+    <filename>pg_xact</filename> transaction XID commit/abort status
+    lookups won't take place during query execution.
    </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.
+    Freezing is a <acronym>WAL</acronym>-logged operation, so when
+    <command>VACUUM</command> freezes a heap page, any copy of the
+    page located on a physical replication standby server will itself
+    be <quote>frozen</quote> shortly thereafter (when the relevant
+    <literal>FREEZE_PAGE</literal> <acronym>WAL</acronym> record is
+    replayed on the standby).  Queries that run on physical
+    replication standbys thereby avoid <filename>pg_xact</filename>
+    lookups when reading from frozen pages, in just the same way as
+    queries that run on the primary server
+    <footnote>
+     <para>
+      In this regard freezing is unlike setting transaction status
+      <quote>hint bits</quote> in tuple headers: setting hint bits
+      doesn't usually need to be <acronym>WAL</acronym>-logged, and
+      can take place on physical replication standby servers without
+      the involvement of the primary server.  The purpose of hint bits
+      is to avoid repeat <filename>pg_xact</filename> lookups for the
+      same tuples, strictly as an optimization.  The purpose of
+      freezing (from the point of view of individual tuples) is to
+      <emphasis>reliably</emphasis> remove each tuple's dependency on
+      <filename>pg_xact</filename>, ultimately making it safe to
+      truncate <filename>pg_xact</filename> from time to time.
+     </para>
+    </footnote>.
    </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.)
+    It can be useful for <command>VACUUM</command> to put off some of
+    the work of freezing, but <command>VACUUM</command> cannot put off
+    freezing forever.  Since on-disk storage of transaction IDs in
+    heap row headers uses a truncated 32-bit representation to save
+    space (rather than the full 64-bit representation), freezing plays
+    a crucial role in enabling <link
+     linkend="aggressive-vacuum">management of the XID address
+     space</link> by <command>VACUUM</command>.  If, for whatever
+    reason, <command>VACUUM</command> is unable to freeze older XIDs
+    on behalf of an application that continues to require new XID
+    allocations, the system will eventually
+    <link linkend="xid-stop-limit">refuse to allocate new transaction IDs</link>.
+    The system generally only enters this state when autovacuum is
+    misconfigured.
    </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>.
+    <xref linkend="guc-vacuum-freeze-min-age"/> controls when freezing
+    takes place.  When <command>VACUUM</command> scans a heap page
+    containing even one XID that has already attained an age exceeding
+    this value, the page is frozen.
+   </para>
+
+   <indexterm>
+    <primary>MultiXact ID</primary>
+    <secondary>Freezing of</secondary>
+   </indexterm>
+
+   <para>
+    <firstterm>MultiXact IDs</firstterm> are used to support row
+    locking by multiple transactions.  Since there is only limited
+    space in a tuple header to store lock information, that
+    information is encoded as a <quote>multiple transaction
+     ID</quote>, or MultiXact ID for short, whenever there is more
+    than one transaction concurrently locking a row.  Information
+    about which transaction IDs are included in any particular
+    MultiXact ID is stored separately in
+    <filename>pg_multixact</filename>, and only the MultiXact ID
+    itself (a 32-bit unsigned integer) appears in the tuple's
+    <structfield>xmax</structfield> field.  This creates a dependency
+    on external transaction status information similar to the
+    dependency that ordinary unfrozen XIDs have on commit status
+    information stored in <filename>pg_xact</filename>.
+    <command>VACUUM</command> must therefore occasionally remove
+    MultiXact IDs from tuples during freezing.
    </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.
+    <xref linkend="guc-vacuum-multixact-freeze-min-age"/> also
+    controls when freezing takes place.   It is analogous to
+    <varname>vacuum_freeze_min_age</varname>, but <quote>age</quote>
+    is expressed in units of MultiXact ID.
+    Lowering <varname>vacuum_multixact_freeze_min_age</varname>
+    <emphasis>forces</emphasis> <command>VACUUM</command> to process
+    <structfield>xmax</structfield> fields containing a MultiXact ID
+    in cases where it would otherwise opt to put off the work of
+    processing <structfield>xmax</structfield> until the next
+    <command>VACUUM</command> <footnote>
+     <para>
+      <quote>Freezing</quote> of <structfield>xmax</structfield>
+      fields (whether they were found to contain an XID or a MultiXact
+      ID) generally means clearing <structfield>xmax</structfield>.
+      <command>VACUUM</command> may occasionally encounter an
+      individual MultiXact ID that must be removed to advance
+      <structfield>relminmxid</structfield> by the required amount,
+      which can only be processed by generating a replacement
+      MultiXact ID (containing just the non-removable subset of member
+      XIDs from the original MultiXact ID), and then setting the
+      tuple's <structfield>xmax</structfield> to the new/replacement
+      MultiXact ID value.
+     </para>
+    </footnote>.  The setting generally doesn't significantly
+    influence the total number of pages <command>VACUUM</command>
+    freezes, even in tables that contain relatively many MultiXact
+    IDs.  This is because <command>VACUUM</command> generally prefers
+    proactively processing for most individual
+    <structfield>xmax</structfield> fields that contain a MultiXact ID
+    (eager proactive processing is typically cheaper).
    </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.)
+    Managing the added <acronym>WAL</acronym> volume from freezing
+    over time is an important consideration for
+    <command>VACUUM</command>.  It is why <command>VACUUM</command>
+    doesn't just freeze every eligible tuple at the earliest
+    opportunity: the <acronym>WAL</acronym> written to freeze a page's
+    tuples <quote>goes to waste</quote> in cases where the resulting
+    frozen tuples are soon deleted or updated anyway.  It's also why
+    <command>VACUUM</command> <emphasis>will</emphasis> freeze all
+    eligible tuples from a heap page once the decision to freeze at
+    least one tuple is taken: at that point the added cost to freeze
+    all eligible tuples eagerly (measured in <quote>extra bytes of
+     <acronym>WAL</acronym> written</quote>) is far lower than the
+    probable cost of deferring freezing until a future
+    <command>VACUUM</command> operation against the same table.
+    Furthermore, once the page is frozen it can generally be marked as
+    all-frozen in the visibility map right away.
    </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>
+   <note>
+    <para>
+     In <productname>PostgreSQL</productname> versions before 16,
+     <command>VACUUM</command> triggered freezing at the level of
+     individual <structfield>xmin</structfield> and
+     <structfield>xmax</structfield> fields.  Freezing only affected
+     the exact XIDs that had already attained an age of
+     <varname>vacuum_freeze_min_age</varname> or greater.
+    </para>
+   </note>
 
    <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> (typically the most recent
-    aggressive VACUUM).  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,
-       greatest(age(c.relfrozenxid),age(t.relfrozenxid)) as age
-FROM pg_class c
-LEFT JOIN pg_class t ON c.reltoastrelid = t.oid
-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.
+    <command>VACUUM</command> also triggers freezing of a page in
+    cases where it already proved necessary to write out a full page
+    image (<acronym>FPI</acronym>) as part of a <acronym>WAL</acronym>
+    record describing how dead tuples were removed <footnote>
+     <para>
+      Actually, the <quote>freeze on an <acronym>FPI</acronym>
+       write</quote> mechanism isn't just triggered whenever
+      <command>VACUUM</command> needed to write an
+      <acronym>FPI</acronym> for torn page protection as part of
+      writing a <literal>PRUNE</literal> <acronym>WAL</acronym> record
+      describing how dead tuples were removed.  The
+      <acronym>FPI</acronym> mechanism can also be triggered when hint
+      bits are set by <command>VACUUM</command>, if and only if doing
+      so necessitates writing an <acronym>FPI</acronym>.
+      <acronym>WAL</acronym>-logging in order to set hint bits is only
+      possible when the <xref linkend="guc-wal-log-hints"/> option is
+      enabled in <filename>postgresql.conf</filename>, or when data
+      checksums were enabled when the cluster was initialized with
+      <xref linkend="app-initdb"/>.
+     </para>
+    </footnote> (see <xref linkend="wal-reliability"/> for background
+    information about how <acronym>FPI</acronym>s provide torn page
+    protection).  This <quote>freeze on an <acronym>FPI</acronym>
+     write</quote> batching mechanism often avoids the need for some
+    future <command>VACUUM</command> operation to write an additional
+    <acronym>FPI</acronym> for the same page as part of a
+    <acronym>WAL</acronym> record describing how live tuples were
+    frozen.  In effect, <command>VACUUM</command> writes slightly more
+    <acronym>WAL</acronym> in the short term with the aim of
+    ultimately needing to write much less <acronym>WAL</acronym> in
+    the long term.
    </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, and the number of
-     newly frozen pages.  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.
+     For tables which receive <command>INSERT</command> operations,
+     but few or no <command>UPDATE</command>/<command>DELETE</command>
+     operations, it may be beneficial to selectively lower <xref
+      linkend="reloption-autovacuum-freeze-min-age"/> for the table.
+     <command>VACUUM</command> may thereby be able to freeze the
+     table's pages <quote>eagerly</quote> during earlier autovacuums
+     triggered by <xref linkend="guc-autovacuum-vacuum-insert-scale-factor"/>.
     </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>
+   <caution>
+    <para>
+     <command>VACUUM</command> may not be able to freeze every tuple's
+     <structfield>xmin</structfield> in relatively rare cases.  The
+     criteria that determines basic eligibility for freezing is the
+     same as the one that determines if a deleted tuple can be
+     removed: the XID-based <literal>removable cutoff</literal> that
+     appears in the server log's autovacuum log reports (controlled by
+     <xref linkend="guc-log-autovacuum-min-duration"/>).
+    </para>
+    <para>
+     In extreme cases, a long-running transaction can hold back every
+     <command>VACUUM</command>'s removable cutoff for so long that the
+     system is forced to activate <link
+      linkend="xid-stop-limit"><literal>xidStopLimit</literal> mode
+      protections</link>.
+    </para>
+   </caution>
 
-   <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:
+   <sect3 id="aggressive-vacuum">
+    <title>Aggressive <command>VACUUM</command></title>
+
+    <indexterm zone="aggressive-vacuum">
+     <primary>transaction ID</primary>
+     <secondary>wraparound</secondary>
+    </indexterm>
+
+    <indexterm>
+     <primary>wraparound</primary>
+     <secondary>of transaction IDs and MultiXact IDs</secondary>
+    </indexterm>
+
+    <para>
+     As noted already, freezing doesn't just allow queries to avoid
+     lookups of subsidiary transaction status information in
+     structures such as <filename>pg_xact</filename>.  Freezing also
+     plays a crucial role in enabling management of the XID address
+     space by <command>VACUUM</command>.  <command>VACUUM</command>
+     maintains information about the oldest unfrozen XID that remains
+     in the table when it uses its <firstterm>aggressive strategy</firstterm>.
+    </para>
+
+    <para>
+     Aggressive <command>VACUUM</command> will update the table's
+     <structname>pg_class</structname>.<structfield>relfrozenxid</structfield>
+     to the value that it determined to be the oldest remaining XID;
+     the table's <structfield>relfrozenxid</structfield>
+     <quote>advances</quote> by a certain number of XIDs.  Aggressive
+     <command>VACUUM</command> may also need to update the
+     <structfield>datfrozenxid</structfield> column of the database's
+     <structname>pg_database</structname> row in turn.
+     <structfield>datfrozenxid</structfield> 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 (the <structfield>relfrozenxid</structfield> that has
+     attained the greatest age) within the database.
+    </para>
+
+    <para>
+     Aggressive <command>VACUUM</command> also maintains the
+     <structname>pg_class</structname>.<structfield>relminmxid</structfield>
+     and <structname>pg_database</structname>.<structfield>datminmxid</structfield>
+     fields.  These are needed to track the oldest MultiXact ID that
+     remains in the table and database, respectively.
+    </para>
+
+    <para>
+     The extra steps performed within every aggressive
+     <command>VACUUM</command> against every table have the overall
+     effect of tracking the oldest remaining unfrozen transaction ID
+     in the entire cluster (every table from every database).
+     Aggressive <command>VACUUM</command>s will (in the aggregate and
+     over time) make sure that the oldest unfrozen transaction ID in
+     the entire system is never too far in the past.
+    </para>
+
+    <note>
+    <title>Managing the Transaction ID Space</title>
+     <para>
+      Freezing removes <emphasis>local</emphasis> dependencies on
+      external transaction status information from individual heap
+      pages.  Advancing <structfield>relfrozenxid</structfield>
+      removes <emphasis>global</emphasis> dependencies from whole
+      tables in turn.
+     </para>
+     <para>
+      The oldest XID in the entire cluster can be thought of as the
+      beginning of the XID space, while the next unallocated XID can
+      be thought of as the end of the XID space.  This space
+      represents the range of XIDs that might still require
+      transaction commit/abort status lookups in <filename>pg_xact</filename>.
+     </para>
+    </note>
+
+    <para>
+     The maximum XID age that the system can tolerate (i.e., the
+     maximum <quote>distance</quote> between the oldest unfrozen
+     transaction ID in any table according to
+     <structname>pg_class</structname>.<structfield>relfrozenxid</structfield>,
+     and the next unallocated transaction ID) is about 2.1 billion
+     transaction IDs.  This <quote>maximum XID age</quote> invariant
+     makes it fundamentally impossible to put off aggressive
+     <command>VACUUM</command>s (and freezing) forever
+     <footnote>
+      <para>
+       Aggressive <command>VACUUM</command>s cannot be put off
+       forever, <emphasis>barring the edge-case where the
+       installation is never expected to consume more than about 2.1
+       billion XIDs</emphasis>.  In practice this has practical
+       relevance.
+      </para>
+     </footnote>.  The invariant imposes an absolute hard limit on how
+     long any table can go without an aggressive <command>VACUUM</command>.
+    </para>
+
+    <para>
+     If the hard limit is ever reached, then the system will activate
+     <link linkend="xid-stop-limit"><literal>xidStopLimit</literal>
+      mode</link>, which temporarily prevents the allocation of new
+     permanent transaction IDs.  The system will only deactive
+     <literal>xidStopLimit</literal> mode when
+     <command>VACUUM</command> (typically run by autovacuum) succeeds
+     in advancing the oldest <literal>datfrozenxid</literal> in the
+     cluster (via an aggressive <command>VACUUM</command> that runs to
+     completion against the table that has the oldest
+     <structfield>relfrozenxid</structfield>).
+    </para>
+
+    <para>
+     The 2.1 billion XIDs <quote>maximum XID age</quote> invariant
+     must be preserved because transaction IDs stored in heap row
+     headers use a truncated 32-bit representation (rather than the
+     full 64-bit representation).  Since all unfrozen transaction IDs
+     from heap tuple headers <emphasis>must</emphasis> be from the
+     same transaction ID epoch (or from a space in the 64-bit
+     representation that spans two adjoining transaction ID epochs),
+     there isn't any need to store a separate epoch field in each
+     tuple header (see <xref linkend="interpreting-xid-stamps"/> for
+     further details).  This scheme has the advantage of requiring
+     much less on-disk storage space than a design that stores an XID
+     epoch alongside each XID stored in each heap tuple header.  It
+     has the disadvantage of constraining the system's ability to
+     allocate new XIDs in the worst case scenario where
+     <literal>xidStopLimit</literal> mode is used to preserve the
+     <quote>maximum XID age</quote> invariant.
+    </para>
+
+    <para>
+     There is only one major runtime behavioral difference between
+     aggressive mode <command>VACUUM</command>s and non-aggressive
+     <command>VACUUM</command>s: only non-aggressive
+     <command>VACUUM</command>s will skip pages that don't have any
+     dead row versions even if those pages still have row versions
+     with old XID values (pages marked as all-visible in the
+     visibility map).  Aggressive <command>VACUUM</command>s can only
+     skip pages that are marked as both all-visible and all-frozen.
+     Consequently, non-aggressive <command>VACUUM</command>s usually
+     won't freeze <emphasis>every</emphasis> page containing an XID
+     that has already attained an age of
+     <varname>vacuum_freeze_min_age</varname> or more.  Failing to
+     freeze older pages during non-aggressive
+     <command>VACUUM</command>s may lead to aggressive
+     <command>VACUUM</command>s that perform a disproportionately
+     large amount of the work of freezing required by one particular
+     table.
+    </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.  Its output includes information about how
+      <structfield>relfrozenxid</structfield> and
+      <structfield>relminmxid</structfield> advanced, and the number
+      of newly frozen pages.  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>
+
+    <note>
+     <para>
+      In practice, most tables require periodic aggressive vacuuming.
+      However, some individual non-aggressive
+      <command>VACUUM</command> operations may be able to advance
+      <structfield>relfrozenxid</structfield> and/or
+      <structfield>relminmxid</structfield>.  Non-aggressive
+      <structfield>relfrozenxid</structfield>/<structfield>relminmxid</structfield>
+      advancement is most common in small, frequently modified tables.
+     </para>
+    </note>
+
+    <para>
+     Most individual tables will eventually need an aggressive
+     <command>VACUUM</command>, which will reliably freeze all pages
+     with XID (or MultiXact ID) values older than
+     <varname>vacuum_freeze_min_age</varname> (or older than
+     <varname>vacuum_multixact_freeze_min_age</varname>), including
+     those from all-visible but not all-frozen pages (and then advance
+     <structname>pg_class</structname>.<structfield>relfrozenxid</structfield>
+     to a value that reflects all that).  <xref
+      linkend="guc-vacuum-freeze-table-age"/> controls when
+     <command>VACUUM</command> must use its aggressive strategy.  If
+     <literal>age(relfrozenxid)</literal> exceeds
+     <varname>vacuum_freeze_table_age</varname> at the start of
+     <command>VACUUM</command>, that <command>VACUUM</command> will
+     use the aggressive strategy; otherwise the standard
+     non-aggressive strategy is used.  Setting
+     <varname>vacuum_freeze_table_age</varname> to 0 forces
+     <command>VACUUM</command> to always use its aggressive strategy.
+    </para>
+   </sect3>
+
+   <sect3 id="anti-wraparound-autovacuums">
+    <title>Anti-Wraparound Autovacuums</title>
+
+    <para>
+     To ensure that every table has its
+     <structfield>relfrozenxid</structfield> advanced at somewhat
+     regular intervals, even in the case of completely static tables,
+     autovacuum runs against 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"/>.  These
+     are <firstterm>anti-wraparound autovacuums</firstterm>.
+     Anti-wraparound autovacuums can happen even when autovacuum is
+     nominally disabled in <filename>postgresql.conf</filename>.
+    </para>
+
+    <para>
+     In practice, all anti-wraparound autovacuums will use
+     <command>VACUUM</command>'s aggressive strategy (if they didn't,
+     then it would defeat the whole purpose of anti-wraparound
+     autovacuuming).  Use of <command>VACUUM</command>'s aggressive
+     strategy is certain, because the effective value of
+     <varname>vacuum_freeze_table_age</varname> is silently
+     <quote>clamped</quote> to a value no greater than 95% of the
+     current value of <varname>autovacuum_freeze_max_age</varname>.
+    </para>
+
+    <para>
+     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>, so that there is a
+     window during which any autovacuum triggered by inserts, updates,
+     or deletes (or any manually issued <command>VACUUM</command>)
+     will become an aggressive <command>VACUUM</command>.  Such
+     <command>VACUUM</command>s will reliably advance
+     <structfield>relfrozenxid</structfield> in passing, even though
+     autovacuum won't have specifically set out to make sure
+     <structfield>relfrozenxid</structfield> advances through
+     anti-wraparound autovacuuming.  Anti-wraparound autovacuums may
+     never be required at all in tables that regularly require
+     vacuuming to <link linkend="vacuum-for-space-recovery">reclaim
+      space from dead tuples</link> and/or to <link
+      linkend="vacuum-for-visibility-map">set pages all-visible in the
+      visibility map</link> (especially if
+     <varname>vacuum_freeze_table_age</varname> is set to a value
+     significantly below
+     <varname>autovacuum_freeze_max_age</varname>).
+    </para>
+
+    <note>
+     <title>Note on terminology</title>
+     <para>
+      Aggressive <command>VACUUM</command> is a special form of
+      <command>VACUUM</command>.  An aggressive
+      <command>VACUUM</command> must advance
+      <structfield>relfrozenxid</structfield> up to an XID value that
+      is no greater than <varname>vacuum_freeze_min_age</varname> XIDs
+      in age as of the <emphasis>start</emphasis> of the
+      <command>VACUUM</command> operation.
+     </para>
+     <para>
+      Anti-wraparound autovacuum is a special form of Autovacuum.  Its
+      purpose is to make sure that
+      <structfield>relfrozenxid</structfield> is advanced when no
+      earlier aggressive <command>VACUUM</command> ran and advanced
+      <structfield>relfrozenxid</structfield> in passing (often
+      because no <command>VACUUM</command> needed to run against the
+      table at all).
+     </para>
+     <para>
+      There is only one runtime behavioral difference between
+      anti-wraparound autovacuums and other autovacuums that happen to
+      end up running an aggressive <command>VACUUM</command>:
+      Anti-wraparound autovacuums <emphasis>cannot be
+       autocancelled</emphasis>.  This means that autovacuum workers
+      that perform anti-wraparound autovacuuming do not yield to
+      conflicting relation-level lock requests (e.g., from
+      <command>ALTER TABLE</command>).  See <xref
+       linkend="autovacuum-lock-conflicts"/> for a full explanation.
+     </para>
+    </note>
+
+    <para>
+     <command>VACUUM</command> also applies <xref
+      linkend="guc-vacuum-multixact-freeze-table-age"/> and <xref
+      linkend="guc-autovacuum-multixact-freeze-max-age"/>.  These are
+     independent MultiXact ID based triggers of aggressive
+     <command>VACUUM</command> (and anti-wraparound autovacuum).  They
+     are applied by following rules analogous to the rules already
+     described for <varname>vacuum_freeze_table_age</varname> and
+     <varname>autovacuum_freeze_max_age</varname>, respectively
+    <footnote>
+     <para>
+      Though note that autovacuum (and <command>VACUUM</command>) use a lower
+      <quote>effective</quote>
+      <varname>autovacuum_multixact_freeze_max_age</varname>
+      value (determined dynamically) to deal with issues with
+      truncation of the <acronym>SLRU</acronym> storage areas, as
+      explained in <xref linkend="vacuum-truncate-xact-status"/>
+     </para>
+    </footnote>.
+    </para>
+
+    <para>
+     It doesn't matter if it was <varname>vacuum_freeze_table_age</varname> or
+     <varname>vacuum_multixact_freeze_table_age</varname> that
+     triggered <command>VACUUM</command>'s decision to use its
+     aggressive strategy.  <emphasis>Every</emphasis> aggressive
+     <command>VACUUM</command> will advance
+     <structfield>relfrozenxid</structfield> and
+     <structfield>relminmxid</structfield> by following the same
+     generic steps at runtime.
+    </para>
+
+    <para>
+     A convenient way to examine information about
+     <structfield>relfrozenxid</structfield> and
+     <structfield>relminmxid</structfield> is to execute queries such as:
+
+<programlisting>
+SELECT c.oid::regclass as table_name,
+greatest(age(c.relfrozenxid),
+         age(t.relfrozenxid)) as xid_age,
+mxid_age(c.relminmxid)
+FROM pg_class c
+LEFT JOIN pg_class t ON c.reltoastrelid = t.oid
+WHERE c.relkind IN ('r', 'm');
+
+SELECT datname,
+age(datfrozenxid) as xid_age,
+mxid_age(datminmxid)
+FROM pg_database;
+</programlisting>
+
+     The <function>age</function> function returns the number of
+     transactions from <structfield>relfrozenxid</structfield> to the
+     next unallocated transaction ID.  The
+     <function>mxid_age</function> function the number of MultiXact
+     IDs from <structfield>relminmxid</structfield> to the next
+     unallocated MultiXact ID.
+    </para>
+
+    <para>
+     The system should always have significant XID allocation slack
+     capacity.  Ideally, the greatest
+     <literal>age(relfrozenxid)</literal>/<literal>age(datfrozenxid)</literal>
+     in the system will never be more than a fraction of the 2.1
+     billion XID hard limit described in <xref
+      linkend="aggressive-vacuum"/>.  The default
+     <varname>vacuum_freeze_table_age</varname> setting of 200 million
+     transactions implies that the system should never use
+     significantly more than about 10% of that hard limit.
+    </para>
+
+    <para>
+     There is little advantage in routinely allowing the greatest
+     <literal>age(relfrozenxid)</literal> in the system to get
+     anywhere near to the 2.1 billion XID hard limit.  Putting off the
+     work of freezing can only reduce the absolute amount of
+     <acronym>WAL</acronym> written by <command>VACUUM</command> when
+     <command>VACUUM</command> thereby completely avoids freezing rows
+     that are deleted before long anyway.  There is little or no
+     disadvantage from lowering <varname>vacuum_freeze_table_age</varname>
+     to make aggressive <command>VACUUM</command>s more frequent, at
+     least in tables where newly frozen pages almost always remain
+     all-frozen forever.  Note also that anything that leads to
+     <structfield>relfrozenxid</structfield> and
+     <structfield>relminmxid</structfield> advancing less frequently
+     (such as a higher <varname>vacuum_freeze_table_age</varname>
+     setting) will also increase the on-disk space required to store
+     additional transaction status information, as described in <xref
+      linkend="vacuum-truncate-xact-status"/>.
+    </para>
+
+   </sect3>
+
+   <sect3 id="xid-stop-limit">
+    <title><literal>xidStopLimit</literal> mode</title>
+    <para>
+     If for some reason autovacuum utterly fails to advance any
+     table's <structfield>relfrozenxid</structfield> or
+     <structfield>relminmxid</structfield> for an extended period, and
+     if XIDs and/or MultiXact IDs continue to be allocated, the system
+     will begin to emit warning messages like this when the database's
+     oldest XIDs reach forty million transactions from the 2.1 billion
+     XID hard limit described in <xref linkend="aggressive-vacuum"/>:
 
 <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:
+     (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 eventually refuse
+     to start any new transactions.  This happens at the point that
+     there are fewer than three million transactions left:
 
 <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 id="vacuum-for-multixact-wraparound">
-    <title>Multixacts and Wraparound</title>
-
-    <indexterm>
-     <primary>MultiXactId</primary>
-    </indexterm>
-
-    <indexterm>
-     <primary>wraparound</primary>
-     <secondary>of multixact IDs</secondary>
-    </indexterm>
-
-    <para>
-     <firstterm>Multixact IDs</firstterm> are used to support row locking by
-     multiple transactions.  Since there is only limited space in a tuple
-     header to store lock information, that information is encoded as
-     a <quote>multiple transaction ID</quote>, or multixact ID for short,
-     whenever there is more than one transaction concurrently locking a
-     row.  Information about which transaction IDs are included in any
-     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.
+     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>
 
     <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.
-    </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>
-
-    <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.
+     In emergencies, <command>VACUUM</command> will take extraordinary
+     measures to avoid <literal>xidStopLimit</literal> mode.  A
+     failsafe mechanism is triggered when the table's
+     <structfield>relfrozenxid</structfield> attains an age of <xref
+      linkend="guc-vacuum-failsafe-age"/> XIDs, or when the table's
+     <structfield>relminmxid</structfield> attains an age of <xref
+      linkend="guc-vacuum-multixact-failsafe-age"/> MultiXact IDs.
+     The failsafe prioritizes advancing
+     <structfield>relfrozenxid</structfield> and/or
+     <structfield>relminmxid</structfield> as quickly as possible.
+     Once the failsafe triggers, <command>VACUUM</command> bypasses
+     all remaining non-essential maintenance tasks, and stops applying
+     any cost-based delay that was in effect.  Any <glossterm
+     linkend="glossary-buffer-access-strategy">Buffer Access
+     Strategy</glossterm> in use will also be disabled.
     </para>
    </sect3>
   </sect2>
@@ -787,12 +1156,23 @@ HINT:  Stop the postmaster and vacuum that database in single-user mode.
    <title>Updating the Visibility Map</title>
 
    <para>
-    Vacuum maintains a <link linkend="storage-vm">visibility
-     map</link> for each table to keep track of which pages contain
-    only tuples that are known to be visible to all active
-    transactions (and all future transactions, until the page is again
-    modified).  This has two purposes.  First, vacuum itself can skip
-    such pages on the next run, since there is nothing to clean up.
+    <command>VACUUM</command> maintains a <link
+     linkend="storage-vm">visibility map</link> for each table to keep
+    track of which pages contain only tuples that are known to be
+    visible to all active transactions (and all future transactions,
+    at least until the page is modified).  A separate bit tracks
+    whether all of the tuples are frozen.
+   </para>
+
+   <para>
+    The visibility map serves two purposes.
+   </para>
+
+   <para>
+    First, <command>VACUUM</command> itself can skip such pages on the
+    next run, since there is nothing to clean up.  Even <link
+     linkend="aggressive-vacuum">aggressive <command>VACUUM</command>s</link>
+    can skip pages that are both all-visible and all-frozen.
    </para>
 
    <para>
@@ -812,6 +1192,65 @@ HINT:  Stop the postmaster and vacuum that database in single-user mode.
    </para>
   </sect2>
 
+  <sect2 id="vacuum-truncate-xact-status">
+   <title>Truncating transaction status information</title>
+
+   <para>
+    Anything that influences when and how
+    <structfield>relfrozenxid</structfield> and
+    <structfield>relminmxid</structfield> advance will also directly
+    affect the high watermark storage overhead needed to store
+    historical transaction status information.  For example,
+    increasing <varname>autovacuum_freeze_max_age</varname> (and
+    <varname>vacuum_freeze_table_age</varname> along with it) will
+    make the <filename>pg_xact</filename> and
+    <filename>pg_commit_ts</filename> subdirectories of the database
+    cluster take more space, because they store the commit/abort
+    status and (if <varname>track_commit_timestamp</varname> is enabled)
+    timestamp of all transactions back to the
+    <varname>datfrozenxid</varname> horizon (the earliest
+    <varname>datfrozenxid</varname> among all databases in the
+    cluster).
+   </para>
+
+   <para>
+    The commit status uses two bits per transaction.  The default
+    <varname>autovacuum_freeze_max_age</varname> setting of 200
+    million transactions translates to about 50MB of
+    <filename>pg_xact</filename> storage.  When
+    <varname>track_commit_timestamp</varname> is enabled, about 2GB of
+    <filename>pg_commit_ts</filename> storage will also be required.
+   </para>
+
+   <para>
+    MultiXact ID status information storage uses two separate
+    underlying <acronym>SLRU</acronym> storage areas:
+    <filename>pg_multixact/members</filename>, and
+    <filename>pg_multixact/offsets</filename>.  There is no simple
+    formula to determine the storage overhead per MultiXact ID, since
+    in general MultiXact IDs have a variable number of member XIDs.
+    Note, however, that if <filename>pg_multixact/members</filename>
+    exceeds 2GB, then the effective value of
+    <varname>autovacuum_multixact_freeze_max_age</varname> used by
+    <command>VACUUM</command> will be lower, resulting in more
+    frequent aggressive mode <command>VACUUM</command>s.
+   </para>
+
+   <para>
+    Truncation of transaction status information is only possible at
+    the end of <command>VACUUM</command>s that advance the earliest
+    <structfield>relfrozenxid</structfield> (in the case of
+    <filename>pg_xact</filename> and
+    <filename>pg_commit_ts</filename>), or the earliest
+    <structfield>relminmxid</structfield> (in the case of
+    <filename>pg_multixact/members</filename> and
+    <filename>pg_multixact/offsets</filename>) among all tables in the
+    entire database (assuming that its the database with the earliest
+    <structfield>datfrozenxid</structfield> and
+    <structfield>datminmxid</structfield> in the entire cluster).
+   </para>
+  </sect2>
+
   <sect2 id="vacuum-for-statistics">
    <title>Updating Planner Statistics</title>
 
@@ -927,7 +1366,7 @@ HINT:  Stop the postmaster and vacuum that database in single-user mode.
    </tip>
 
   </sect2>
-</sect1>
+ </sect1>
 
 
  <sect1 id="routine-reindex">
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 10ef699fa..8aa332fcf 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1515,7 +1515,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="freezing-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..ede50d6f7 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="freezing-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 57bc4c23e..95efe7d36 100644
--- a/doc/src/sgml/ref/vacuum.sgml
+++ b/doc/src/sgml/ref/vacuum.sgml
@@ -123,7 +123,9 @@ VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] [ ANALYZE ] [ <replaceable class="paramet
     <term><literal>FREEZE</literal></term>
     <listitem>
      <para>
-      Selects aggressive <quote>freezing</quote> of tuples.
+      Makes <quote>freezing</quote> <emphasis>maximally</emphasis>
+      aggressive, and forces <command>VACUUM</command> to use its
+      <link linkend="aggressive-vacuum">aggressive strategy</link>.
       Specifying <literal>FREEZE</literal> is equivalent to performing
       <command>VACUUM</command> with the
       <xref linkend="guc-vacuum-freeze-min-age"/> and
@@ -219,7 +221,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="freezing-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 da2393783..b61d523c2 100644
--- a/doc/src/sgml/ref/vacuumdb.sgml
+++ b/doc/src/sgml/ref/vacuumdb.sgml
@@ -233,7 +233,7 @@ PostgreSQL documentation
         ID age of at least <replaceable class="parameter">mxid_age</replaceable>.
         This setting is useful for prioritizing tables to process to prevent
         multixact ID wraparound (see
-        <xref linkend="vacuum-for-multixact-wraparound"/>).
+        <xref linkend="freezing-xid-space"/>).
        </para>
        <para>
         For the purposes of this option, the multixact ID age of a relation is
@@ -254,7 +254,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="freezing-xid-space"/>).
        </para>
        <para>
         For the purposes of this option, the transaction ID age of a relation
diff --git a/doc/src/sgml/xact.sgml b/doc/src/sgml/xact.sgml
index 0762442e1..e372a7875 100644
--- a/doc/src/sgml/xact.sgml
+++ b/doc/src/sgml/xact.sgml
@@ -185,7 +185,7 @@
    rows and can be inspected using the <xref linkend="pgrowlocks"/>
    extension.  Row-level read locks might also require the assignment
    of multixact IDs (<literal>mxid</literal>;  see <xref
-   linkend="vacuum-for-multixact-wraparound"/>).
+   linkend="freezing-xid-space"/>).
   </para>
  </sect1>
 
-- 
2.40.1

