Add PROCESS_MAIN to VACUUM

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
This commit is contained in:
Michael Paquier 2023-03-06 16:41:05 +09:00
parent 46d490ac19
commit 4211fbd841
10 changed files with 156 additions and 15 deletions

View 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>

View 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>

View 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.

View 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);

View 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");

View 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\).*;/,

View 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"));

View 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.

View 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

View 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;