Add PROCESS_MAIN to VACUUM
authorMichael Paquier <michael@paquier.xyz>
Mon, 6 Mar 2023 07:41:05 +0000 (16:41 +0900)
committerMichael Paquier <michael@paquier.xyz>
Mon, 6 Mar 2023 07:41:05 +0000 (16:41 +0900)
Disabling this option is useful to run VACUUM (with or without FULL) on
only the toast table of a relation, bypassing the main relation.  This
option is enabled by default.

Running directly VACUUM on a toast table was already possible without
this feature, by using the non-deterministic name of a toast relation
(as of pg_toast.pg_toast_N, where N would be the OID of the parent
relation) in the VACUUM command, and it required a scan of pg_class to
know the name of the toast table.  So this feature is basically a
shortcut to be able to run VACUUM or VACUUM FULL on a toast relation,
using only the name of the parent relation.

A new switch called --no-process-main is added to vacuumdb, to work as
an equivalent of PROCESS_MAIN.

Regression tests are added to cover VACUUM and VACUUM FULL, looking at
pg_stat_all_tables.vacuum_count to see how many vacuums have run on
each table, main or toast.

Author: Nathan Bossart
Reviewed-by: Masahiko Sawada
Discussion: https://postgr.es/m/20221230000028.GA435655@nathanxps13

doc/src/sgml/ref/vacuum.sgml
doc/src/sgml/ref/vacuumdb.sgml
src/backend/commands/vacuum.c
src/backend/postmaster/autovacuum.c
src/bin/psql/tab-complete.c
src/bin/scripts/t/100_vacuumdb.pl
src/bin/scripts/vacuumdb.c
src/include/commands/vacuum.h
src/test/regress/expected/vacuum.out
src/test/regress/sql/vacuum.sql

index 545b23b54f6462a87bc4d05ec6618901fdfeb723..b6d30b576486771fe2796074fa06ce9ab339fc8e 100644 (file)
@@ -33,6 +33,7 @@ VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] [ ANALYZE ] [ <replaceable class="paramet
     DISABLE_PAGE_SKIPPING [ <replaceable class="parameter">boolean</replaceable> ]
     SKIP_LOCKED [ <replaceable class="parameter">boolean</replaceable> ]
     INDEX_CLEANUP { AUTO | ON | OFF }
+    PROCESS_MAIN [ <replaceable class="parameter">boolean</replaceable> ]
     PROCESS_TOAST [ <replaceable class="parameter">boolean</replaceable> ]
     TRUNCATE [ <replaceable class="parameter">boolean</replaceable> ]
     PARALLEL <replaceable class="parameter">integer</replaceable>
@@ -238,6 +239,18 @@ VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] [ ANALYZE ] [ <replaceable class="paramet
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term><literal>PROCESS_MAIN</literal></term>
+    <listitem>
+     <para>
+      Specifies that <command>VACUUM</command> should attempt to process the
+      main relation. This is usually the desired behavior and is the default.
+      Setting this option to false may be useful when it is only necessary to
+      vacuum a relation's corresponding <literal>TOAST</literal> table.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term><literal>PROCESS_TOAST</literal></term>
     <listitem>
index 841aced3bd5a98762d75902a6d9972f16399deba..74bac2d4ba5fae1d1a9efbfa235e6cfff5e7bf62 100644 (file)
@@ -317,6 +317,21 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-process-main</option></term>
+      <listitem>
+       <para>
+        Skip the main relation.
+       </para>
+       <note>
+        <para>
+         This option is only available for servers running
+         <productname>PostgreSQL</productname> 16 and later.
+        </para>
+       </note>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-process-toast</option></term>
       <listitem>
index aa79d9de4d419eaed4b1cce6829659f21fa3c55f..580f96649912149cd427c57f184f663d5f7bd391 100644 (file)
@@ -115,6 +115,7 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
        bool            freeze = false;
        bool            full = false;
        bool            disable_page_skipping = false;
+       bool            process_main = true;
        bool            process_toast = true;
        bool            skip_database_stats = false;
        bool            only_database_stats = false;
@@ -168,6 +169,8 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
                                        params.index_cleanup = get_vacoptval_from_boolean(opt);
                        }
                }
+               else if (strcmp(opt->defname, "process_main") == 0)
+                       process_main = defGetBoolean(opt);
                else if (strcmp(opt->defname, "process_toast") == 0)
                        process_toast = defGetBoolean(opt);
                else if (strcmp(opt->defname, "truncate") == 0)
@@ -224,6 +227,7 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
                (freeze ? VACOPT_FREEZE : 0) |
                (full ? VACOPT_FULL : 0) |
                (disable_page_skipping ? VACOPT_DISABLE_PAGE_SKIPPING : 0) |
+               (process_main ? VACOPT_PROCESS_MAIN : 0) |
                (process_toast ? VACOPT_PROCESS_TOAST : 0) |
                (skip_database_stats ? VACOPT_SKIP_DATABASE_STATS : 0) |
                (only_database_stats ? VACOPT_ONLY_DATABASE_STATS : 0);
@@ -367,9 +371,10 @@ vacuum(List *relations, VacuumParams *params,
                        ereport(ERROR,
                                        (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
                                         errmsg("ONLY_DATABASE_STATS cannot be specified with a list of tables")));
-               /* don't require people to turn off PROCESS_TOAST explicitly */
+               /* don't require people to turn off PROCESS_TOAST/MAIN explicitly */
                if (params->options & ~(VACOPT_VACUUM |
                                                                VACOPT_VERBOSE |
+                                                               VACOPT_PROCESS_MAIN |
                                                                VACOPT_PROCESS_TOAST |
                                                                VACOPT_ONLY_DATABASE_STATS))
                        ereport(ERROR,
@@ -2031,10 +2036,12 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params, bool skip_privs)
        /*
         * Remember the relation's TOAST relation for later, if the caller asked
         * us to process it.  In VACUUM FULL, though, the toast table is
-        * automatically rebuilt by cluster_rel so we shouldn't recurse to it.
+        * automatically rebuilt by cluster_rel so we shouldn't recurse to it,
+        * unless PROCESS_MAIN is disabled.
         */
        if ((params->options & VACOPT_PROCESS_TOAST) != 0 &&
-               (params->options & VACOPT_FULL) == 0)
+               ((params->options & VACOPT_FULL) == 0 ||
+                (params->options & VACOPT_PROCESS_MAIN) == 0))
                toast_relid = rel->rd_rel->reltoastrelid;
        else
                toast_relid = InvalidOid;
@@ -2053,7 +2060,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params, bool skip_privs)
        /*
         * Do the actual work --- either FULL or "lazy" vacuum
         */
-       if (params->options & VACOPT_FULL)
+       if ((params->options & VACOPT_FULL) &&
+               (params->options & VACOPT_PROCESS_MAIN))
        {
                ClusterParams cluster_params = {0};
 
@@ -2067,7 +2075,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params, bool skip_privs)
                /* VACUUM FULL is now a variant of CLUSTER; see cluster.c */
                cluster_rel(relid, InvalidOid, &cluster_params);
        }
-       else
+       else if (params->options & VACOPT_PROCESS_MAIN)
                table_relation_vacuum(rel, params, vac_strategy);
 
        /* Roll back any GUC changes executed by index functions */
@@ -2094,7 +2102,15 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params, bool skip_privs)
         * totally unimportant for toast relations.
         */
        if (toast_relid != InvalidOid)
-               vacuum_rel(toast_relid, NULL, params, true);
+       {
+               VacuumParams toast_vacuum_params;
+
+               /* force VACOPT_PROCESS_MAIN so vacuum_rel() processes it */
+               memcpy(&toast_vacuum_params, params, sizeof(VacuumParams));
+               toast_vacuum_params.options |= VACOPT_PROCESS_MAIN;
+
+               vacuum_rel(toast_relid, NULL, &toast_vacuum_params, true);
+       }
 
        /*
         * Now release the session-level lock on the main table.
index ff6149a17937c16314df7e1d5da6bbb478643b73..c0e2e00a7e3e6cdbea9cfc568db2d8e6469252a7 100644 (file)
@@ -2860,7 +2860,9 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
                 * skip vac_update_datfrozenxid(); we'll do that separately.
                 */
                tab->at_params.options =
-                       (dovacuum ? (VACOPT_VACUUM | VACOPT_SKIP_DATABASE_STATS) : 0) |
+                       (dovacuum ? (VACOPT_VACUUM |
+                                                VACOPT_PROCESS_MAIN |
+                                                VACOPT_SKIP_DATABASE_STATS) : 0) |
                        (doanalyze ? VACOPT_ANALYZE : 0) |
                        (!wraparound ? VACOPT_SKIP_LOCKED : 0);
 
index 5e1882eaeabd54e02a463337d1d912788df3ec45..8f12af799be11be6d833183f7e677be2f4df7a5e 100644 (file)
@@ -4618,10 +4618,10 @@ psql_completion(const char *text, int start, int end)
                if (ends_with(prev_wd, '(') || ends_with(prev_wd, ','))
                        COMPLETE_WITH("FULL", "FREEZE", "ANALYZE", "VERBOSE",
                                                  "DISABLE_PAGE_SKIPPING", "SKIP_LOCKED",
-                                                 "INDEX_CLEANUP", "PROCESS_TOAST",
+                                                 "INDEX_CLEANUP", "PROCESS_MAIN", "PROCESS_TOAST",
                                                  "TRUNCATE", "PARALLEL", "SKIP_DATABASE_STATS",
                                                  "ONLY_DATABASE_STATS");
-               else if (TailMatches("FULL|FREEZE|ANALYZE|VERBOSE|DISABLE_PAGE_SKIPPING|SKIP_LOCKED|PROCESS_TOAST|TRUNCATE|SKIP_DATABASE_STATS|ONLY_DATABASE_STATS"))
+               else if (TailMatches("FULL|FREEZE|ANALYZE|VERBOSE|DISABLE_PAGE_SKIPPING|SKIP_LOCKED|PROCESS_MAIN|PROCESS_TOAST|TRUNCATE|SKIP_DATABASE_STATS|ONLY_DATABASE_STATS"))
                        COMPLETE_WITH("ON", "OFF");
                else if (TailMatches("INDEX_CLEANUP"))
                        COMPLETE_WITH("AUTO", "ON", "OFF");
index 3cfbaaec0d4ba3a373987ca7771e8262f6b6b66b..46101899ae7d786c3a1a705445009716f3f8b339 100644 (file)
@@ -65,6 +65,13 @@ $node->issues_sql_like(
 $node->command_fails(
        [ 'vacuumdb', '--analyze-only', '--no-truncate', 'postgres' ],
        '--analyze-only and --no-truncate specified together');
+$node->issues_sql_like(
+       [ 'vacuumdb', '--no-process-main', 'postgres' ],
+       qr/statement: VACUUM \(PROCESS_MAIN FALSE, SKIP_DATABASE_STATS\).*;/,
+       'vacuumdb --no-process-main');
+$node->command_fails(
+       [ 'vacuumdb', '--analyze-only', '--no-process-main', 'postgres' ],
+       '--analyze-only and --no-process_main specified together');
 $node->issues_sql_like(
        [ 'vacuumdb', '--no-process-toast', 'postgres' ],
        qr/statement: VACUUM \(PROCESS_TOAST FALSE, SKIP_DATABASE_STATS\).*;/,
index 58b894216b800a50d3d54695b7f1a584f97e3efb..39be265b5bb3956c3d3701630ec345525ae92464 100644 (file)
@@ -43,6 +43,7 @@ typedef struct vacuumingOptions
        bool            no_index_cleanup;
        bool            force_index_cleanup;
        bool            do_truncate;
+       bool            process_main;
        bool            process_toast;
        bool            skip_database_stats;
 } vacuumingOptions;
@@ -121,6 +122,7 @@ main(int argc, char *argv[])
                {"force-index-cleanup", no_argument, NULL, 9},
                {"no-truncate", no_argument, NULL, 10},
                {"no-process-toast", no_argument, NULL, 11},
+               {"no-process-main", no_argument, NULL, 12},
                {NULL, 0, NULL, 0}
        };
 
@@ -148,6 +150,7 @@ main(int argc, char *argv[])
        vacopts.no_index_cleanup = false;
        vacopts.force_index_cleanup = false;
        vacopts.do_truncate = true;
+       vacopts.process_main = true;
        vacopts.process_toast = true;
 
        pg_logging_init(argv[0]);
@@ -260,6 +263,9 @@ main(int argc, char *argv[])
                        case 11:
                                vacopts.process_toast = false;
                                break;
+                       case 12:
+                               vacopts.process_main = false;
+                               break;
                        default:
                                /* getopt_long already emitted a complaint */
                                pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -312,6 +318,9 @@ main(int argc, char *argv[])
                if (!vacopts.do_truncate)
                        pg_fatal("cannot use the \"%s\" option when performing only analyze",
                                         "no-truncate");
+               if (!vacopts.process_main)
+                       pg_fatal("cannot use the \"%s\" option when performing only analyze",
+                                        "no-process-main");
                if (!vacopts.process_toast)
                        pg_fatal("cannot use the \"%s\" option when performing only analyze",
                                         "no-process-toast");
@@ -508,6 +517,13 @@ vacuum_one_database(ConnParams *cparams,
                                 "no-truncate", "12");
        }
 
+       if (!vacopts->process_main && PQserverVersion(conn) < 160000)
+       {
+               PQfinish(conn);
+               pg_fatal("cannot use the \"%s\" option on server versions older than PostgreSQL %s",
+                                "no-process-main", "16");
+       }
+
        if (!vacopts->process_toast && PQserverVersion(conn) < 140000)
        {
                PQfinish(conn);
@@ -976,6 +992,13 @@ prepare_vacuum_command(PQExpBuffer sql, int serverVersion,
                                appendPQExpBuffer(sql, "%sTRUNCATE FALSE", sep);
                                sep = comma;
                        }
+                       if (!vacopts->process_main)
+                       {
+                               /* PROCESS_MAIN is supported since v16 */
+                               Assert(serverVersion >= 160000);
+                               appendPQExpBuffer(sql, "%sPROCESS_MAIN FALSE", sep);
+                               sep = comma;
+                       }
                        if (!vacopts->process_toast)
                        {
                                /* PROCESS_TOAST is supported since v14 */
@@ -1090,6 +1113,7 @@ help(const char *progname)
        printf(_("      --min-mxid-age=MXID_AGE     minimum multixact ID age of tables to vacuum\n"));
        printf(_("      --min-xid-age=XID_AGE       minimum transaction ID age of tables to vacuum\n"));
        printf(_("      --no-index-cleanup          don't remove index entries that point to dead tuples\n"));
+       printf(_("      --no-process-main           skip the main relation\n"));
        printf(_("      --no-process-toast          skip the TOAST table associated with the table to vacuum\n"));
        printf(_("      --no-truncate               don't truncate empty pages at the end of the table\n"));
        printf(_("  -n, --schema=PATTERN            vacuum tables in the specified schema(s) only\n"));
index 689dbb7702435fa74ad45886647699ad0edb6471..bdfd96cfec6c1cf5b2d91e18a0b5aa66b0b89f1a 100644 (file)
@@ -186,10 +186,11 @@ typedef struct VacAttrStats
 #define VACOPT_FREEZE 0x08             /* FREEZE option */
 #define VACOPT_FULL 0x10               /* FULL (non-concurrent) vacuum */
 #define VACOPT_SKIP_LOCKED 0x20 /* skip if cannot get lock */
-#define VACOPT_PROCESS_TOAST 0x40      /* process the TOAST table, if any */
-#define VACOPT_DISABLE_PAGE_SKIPPING 0x80      /* don't skip any pages */
-#define VACOPT_SKIP_DATABASE_STATS 0x100       /* skip vac_update_datfrozenxid() */
-#define VACOPT_ONLY_DATABASE_STATS 0x200       /* only vac_update_datfrozenxid() */
+#define VACOPT_PROCESS_MAIN 0x40       /* process main relation */
+#define VACOPT_PROCESS_TOAST 0x80      /* process the TOAST table, if any */
+#define VACOPT_DISABLE_PAGE_SKIPPING 0x100     /* don't skip any pages */
+#define VACOPT_SKIP_DATABASE_STATS 0x200       /* skip vac_update_datfrozenxid() */
+#define VACOPT_ONLY_DATABASE_STATS 0x400       /* only vac_update_datfrozenxid() */
 
 /*
  * Values used by index_cleanup and truncate params.
index 07271e1660e37bd48f56eb2752640eb7c1b2833d..e5a312182eba080b2d9a256111acdc3fc83d2011 100644 (file)
@@ -281,7 +281,8 @@ CREATE TABLE vac_option_tab (a INT, t TEXT);
 INSERT INTO vac_option_tab SELECT a, 't' || a FROM generate_series(1, 10) AS a;
 ALTER TABLE vac_option_tab ALTER COLUMN t SET STORAGE EXTERNAL;
 -- Check the number of vacuums done on table vac_option_tab and on its
--- toast relation, to check that PROCESS_TOAST works on what it should.
+-- toast relation, to check that PROCESS_TOAST and PROCESS_MAIN work on
+-- what they should.
 CREATE VIEW vac_option_tab_counts AS
   SELECT CASE WHEN c.relname IS NULL
     THEN 'main' ELSE 'toast' END as rel,
@@ -308,6 +309,47 @@ SELECT * FROM vac_option_tab_counts;
 
 VACUUM (PROCESS_TOAST FALSE, FULL) vac_option_tab; -- error
 ERROR:  PROCESS_TOAST required with VACUUM FULL
+-- PROCESS_MAIN option
+-- Only the toast table is processed.
+VACUUM (PROCESS_MAIN FALSE) vac_option_tab;
+SELECT * FROM vac_option_tab_counts;
+  rel  | vacuum_count 
+-------+--------------
+ main  |            2
+ toast |            2
+(2 rows)
+
+-- Nothing is processed.
+VACUUM (PROCESS_MAIN FALSE, PROCESS_TOAST FALSE) vac_option_tab;
+SELECT * FROM vac_option_tab_counts;
+  rel  | vacuum_count 
+-------+--------------
+ main  |            2
+ toast |            2
+(2 rows)
+
+-- Check if the filenodes nodes have been updated as wanted after FULL.
+SELECT relfilenode AS main_filenode FROM pg_class
+  WHERE relname = 'vac_option_tab' \gset
+SELECT t.relfilenode AS toast_filenode FROM pg_class c, pg_class t
+  WHERE c.reltoastrelid = t.oid AND c.relname = 'vac_option_tab' \gset
+-- Only the toast relation is processed.
+VACUUM (PROCESS_MAIN FALSE, FULL) vac_option_tab;
+SELECT relfilenode = :main_filenode AS is_same_main_filenode
+  FROM pg_class WHERE relname = 'vac_option_tab';
+ is_same_main_filenode 
+-----------------------
+ t
+(1 row)
+
+SELECT t.relfilenode = :toast_filenode AS is_same_toast_filenode
+  FROM pg_class c, pg_class t
+  WHERE c.reltoastrelid = t.oid AND c.relname = 'vac_option_tab';
+ is_same_toast_filenode 
+------------------------
+ f
+(1 row)
+
 -- SKIP_DATABASE_STATS option
 VACUUM (SKIP_DATABASE_STATS) vactst;
 -- ONLY_DATABASE_STATS option
index 364d297a6e408680d8bf8d1762d58c25e703d214..a1fad43657cc721ac2c7b53f5355c9a5b86f5394 100644 (file)
@@ -236,7 +236,8 @@ CREATE TABLE vac_option_tab (a INT, t TEXT);
 INSERT INTO vac_option_tab SELECT a, 't' || a FROM generate_series(1, 10) AS a;
 ALTER TABLE vac_option_tab ALTER COLUMN t SET STORAGE EXTERNAL;
 -- Check the number of vacuums done on table vac_option_tab and on its
--- toast relation, to check that PROCESS_TOAST works on what it should.
+-- toast relation, to check that PROCESS_TOAST and PROCESS_MAIN work on
+-- what they should.
 CREATE VIEW vac_option_tab_counts AS
   SELECT CASE WHEN c.relname IS NULL
     THEN 'main' ELSE 'toast' END as rel,
@@ -251,6 +252,26 @@ VACUUM (PROCESS_TOAST FALSE) vac_option_tab;
 SELECT * FROM vac_option_tab_counts;
 VACUUM (PROCESS_TOAST FALSE, FULL) vac_option_tab; -- error
 
+-- PROCESS_MAIN option
+-- Only the toast table is processed.
+VACUUM (PROCESS_MAIN FALSE) vac_option_tab;
+SELECT * FROM vac_option_tab_counts;
+-- Nothing is processed.
+VACUUM (PROCESS_MAIN FALSE, PROCESS_TOAST FALSE) vac_option_tab;
+SELECT * FROM vac_option_tab_counts;
+-- Check if the filenodes nodes have been updated as wanted after FULL.
+SELECT relfilenode AS main_filenode FROM pg_class
+  WHERE relname = 'vac_option_tab' \gset
+SELECT t.relfilenode AS toast_filenode FROM pg_class c, pg_class t
+  WHERE c.reltoastrelid = t.oid AND c.relname = 'vac_option_tab' \gset
+-- Only the toast relation is processed.
+VACUUM (PROCESS_MAIN FALSE, FULL) vac_option_tab;
+SELECT relfilenode = :main_filenode AS is_same_main_filenode
+  FROM pg_class WHERE relname = 'vac_option_tab';
+SELECT t.relfilenode = :toast_filenode AS is_same_toast_filenode
+  FROM pg_class c, pg_class t
+  WHERE c.reltoastrelid = t.oid AND c.relname = 'vac_option_tab';
+
 -- SKIP_DATABASE_STATS option
 VACUUM (SKIP_DATABASE_STATS) vactst;