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;