diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml index 85c0ec1b31..1a8184e306 100644 --- a/doc/src/sgml/ref/create_table.sgml +++ b/doc/src/sgml/ref/create_table.sgml @@ -1389,6 +1389,21 @@ WITH ( MODULUS numeric_literal, REM + + vacuum_index_cleanup (boolean) + + + Enables or disables index cleanup when VACUUM is + run on this table. The default value is true. + Disabling index cleanup can speed up VACUUM very + significantly, but may also lead to severely bloated indexes if table + modifications are frequent. The INDEX_CLEANUP + parameter to , if specified, overrides + the value of this option. + + + + autovacuum_vacuum_threshold, toast.autovacuum_vacuum_threshold (integer) diff --git a/doc/src/sgml/ref/vacuum.sgml b/doc/src/sgml/ref/vacuum.sgml index 906d0c2ad7..fdd8151220 100644 --- a/doc/src/sgml/ref/vacuum.sgml +++ b/doc/src/sgml/ref/vacuum.sgml @@ -32,6 +32,7 @@ VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] [ ANALYZE ] [ boolean ] DISABLE_PAGE_SKIPPING [ boolean ] SKIP_LOCKED [ boolean ] + INDEX_CLEANUP [ boolean ] and table_and_columns is: @@ -181,6 +182,28 @@ VACUUM [ FULL ] [ FREEZE ] [ VERBOSE ] [ ANALYZE ] [ ). However, if index + cleanup is not performed regularly, performance may suffer, because + as the table is modified, indexes will accumulate dead tuples + and the table itself will accumulate dead line pointers that cannot be + removed until index cleanup is completed. This option has no effect + for tables that do not have an index and is ignored if the + FULL is used. + + + + boolean diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c index b58a1f7a72..e2c0de352a 100644 --- a/src/backend/access/common/reloptions.c +++ b/src/backend/access/common/reloptions.c @@ -138,6 +138,15 @@ static relopt_bool boolRelOpts[] = }, false }, + { + { + "vacuum_index_cleanup", + "Enables index vacuuming and index cleanup", + RELOPT_KIND_HEAP, + ShareUpdateExclusiveLock + }, + true + }, /* list terminator */ {{NULL}} }; @@ -1388,7 +1397,9 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind) {"parallel_workers", RELOPT_TYPE_INT, offsetof(StdRdOptions, parallel_workers)}, {"vacuum_cleanup_index_scale_factor", RELOPT_TYPE_REAL, - offsetof(StdRdOptions, vacuum_cleanup_index_scale_factor)} + offsetof(StdRdOptions, vacuum_cleanup_index_scale_factor)}, + {"vacuum_index_cleanup", RELOPT_TYPE_BOOL, + offsetof(StdRdOptions, vacuum_index_cleanup)} }; options = parseRelOptions(reloptions, validate, kind, &numoptions); diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c index 392b35ebb7..c9d83128d5 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -112,8 +112,8 @@ typedef struct LVRelStats { - /* hasindex = true means two-pass strategy; false means one-pass */ - bool hasindex; + /* useindex = true means two-pass strategy; false means one-pass */ + bool useindex; /* Overall statistics about rel */ BlockNumber old_rel_pages; /* previous value of pg_class.relpages */ BlockNumber rel_pages; /* total number of pages */ @@ -125,6 +125,8 @@ typedef struct LVRelStats double new_rel_tuples; /* new estimated total # of tuples */ double new_live_tuples; /* new estimated total # of live tuples */ double new_dead_tuples; /* new estimated total # of dead tuples */ + double nleft_dead_tuples; /* # of dead tuples we left */ + double nleft_dead_itemids; /* # of dead item pointers we left */ BlockNumber pages_removed; double tuples_deleted; BlockNumber nonempty_pages; /* actually, last nonempty page + 1 */ @@ -150,7 +152,7 @@ static BufferAccessStrategy vac_strategy; /* non-export function prototypes */ -static void lazy_scan_heap(Relation onerel, int options, +static void lazy_scan_heap(Relation onerel, VacuumParams *params, LVRelStats *vacrelstats, Relation *Irel, int nindexes, bool aggressive); static void lazy_vacuum_heap(Relation onerel, LVRelStats *vacrelstats, BlockNumber nblocks); @@ -209,6 +211,7 @@ heap_vacuum_rel(Relation onerel, VacuumParams *params, MultiXactId new_min_multi; Assert(params != NULL); + Assert(params->index_cleanup != VACOPT_TERNARY_DEFAULT); /* measure elapsed time iff autovacuum logging requires it */ if (IsAutoVacuumWorkerProcess() && params->log_min_duration >= 0) @@ -275,10 +278,11 @@ heap_vacuum_rel(Relation onerel, VacuumParams *params, /* Open all indexes of the relation */ vac_open_indexes(onerel, RowExclusiveLock, &nindexes, &Irel); - vacrelstats->hasindex = (nindexes > 0); + vacrelstats->useindex = (nindexes > 0 && + params->index_cleanup == VACOPT_TERNARY_ENABLED); /* Do the vacuuming */ - lazy_scan_heap(onerel, params->options, vacrelstats, Irel, nindexes, aggressive); + lazy_scan_heap(onerel, params, vacrelstats, Irel, nindexes, aggressive); /* Done with indexes */ vac_close_indexes(nindexes, Irel, NoLock); @@ -349,7 +353,7 @@ heap_vacuum_rel(Relation onerel, VacuumParams *params, new_rel_pages, new_live_tuples, new_rel_allvisible, - vacrelstats->hasindex, + nindexes > 0, new_frozen_xid, new_min_multi, false); @@ -419,6 +423,12 @@ heap_vacuum_rel(Relation onerel, VacuumParams *params, vacrelstats->new_rel_tuples, vacrelstats->new_dead_tuples, OldestXmin); + if (vacrelstats->nleft_dead_tuples > 0 || + vacrelstats->nleft_dead_itemids > 0) + appendStringInfo(&buf, + _("%.0f tuples and %.0f item identifiers are left as dead.\n"), + vacrelstats->nleft_dead_tuples, + vacrelstats->nleft_dead_itemids); appendStringInfo(&buf, _("buffer usage: %d hits, %d misses, %d dirtied\n"), VacuumPageHit, @@ -485,7 +495,7 @@ vacuum_log_cleanup_info(Relation rel, LVRelStats *vacrelstats) * reference them have been killed. */ static void -lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats, +lazy_scan_heap(Relation onerel, VacuumParams *params, LVRelStats *vacrelstats, Relation *Irel, int nindexes, bool aggressive) { BlockNumber nblocks, @@ -501,7 +511,10 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats, live_tuples, /* live tuples (reltuples estimate) */ tups_vacuumed, /* tuples cleaned up by vacuum */ nkeep, /* dead-but-not-removable tuples */ - nunused; /* unused item pointers */ + nunused, /* unused item pointers */ + nleft_dead_tuples, /* tuples we left as dead */ + nleft_dead_itemids; /* item pointers we left as dead, + * includes nleft_dead_tuples. */ IndexBulkDeleteResult **indstats; int i; PGRUsage ru0; @@ -534,6 +547,7 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats, empty_pages = vacuumed_pages = 0; next_fsm_block_to_vacuum = (BlockNumber) 0; num_tuples = live_tuples = tups_vacuumed = nkeep = nunused = 0; + nleft_dead_itemids = nleft_dead_tuples = 0; indstats = (IndexBulkDeleteResult **) palloc0(nindexes * sizeof(IndexBulkDeleteResult *)); @@ -599,7 +613,7 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats, * be replayed on any hot standby, where it can be disruptive. */ next_unskippable_block = 0; - if ((options & VACOPT_DISABLE_PAGE_SKIPPING) == 0) + if ((params->options & VACOPT_DISABLE_PAGE_SKIPPING) == 0) { while (next_unskippable_block < nblocks) { @@ -654,7 +668,7 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats, { /* Time to advance next_unskippable_block */ next_unskippable_block++; - if ((options & VACOPT_DISABLE_PAGE_SKIPPING) == 0) + if ((params->options & VACOPT_DISABLE_PAGE_SKIPPING) == 0) { while (next_unskippable_block < nblocks) { @@ -1070,7 +1084,17 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats, HeapTupleIsHeapOnly(&tuple)) nkeep += 1; else + { tupgone = true; /* we can delete the tuple */ + + /* + * Since this dead tuple will not be vacuumed and + * ignored when index cleanup is disabled we count + * count it for reporting. + */ + if (params->index_cleanup == VACOPT_TERNARY_ENABLED) + nleft_dead_tuples++; + } all_visible = false; break; case HEAPTUPLE_LIVE: @@ -1222,15 +1246,33 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats, } /* - * If there are no indexes then we can vacuum the page right now - * instead of doing a second scan. + * If there are no indexes we can vacuum the page right now instead of + * doing a second scan. Also we don't do that but forget dead tuples + * when index cleanup is disabled. */ - if (nindexes == 0 && - vacrelstats->num_dead_tuples > 0) + if (!vacrelstats->useindex && vacrelstats->num_dead_tuples > 0) { - /* Remove tuples from heap */ - lazy_vacuum_page(onerel, blkno, buf, 0, vacrelstats, &vmbuffer); - has_dead_tuples = false; + if (nindexes == 0) + { + /* Remove tuples from heap if the table has no index */ + lazy_vacuum_page(onerel, blkno, buf, 0, vacrelstats, &vmbuffer); + vacuumed_pages++; + has_dead_tuples = false; + } + else + { + /* + * Here, we have indexes but index cleanup is disabled. Instead of + * vacuuming the dead tuples on the heap, we just forget them. + * + * Note that vacrelstats->dead_tuples could have tuples which + * became dead after HOT-pruning but are not marked dead yet. + * We do not process them because it's a very rare condition, and + * the next vacuum will process them anyway. + */ + Assert(params->index_cleanup == VACOPT_TERNARY_DISABLED); + nleft_dead_itemids += vacrelstats->num_dead_tuples; + } /* * Forget the now-vacuumed tuples, and press on, but be careful @@ -1238,7 +1280,6 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats, * valid. */ vacrelstats->num_dead_tuples = 0; - vacuumed_pages++; /* * Periodically do incremental FSM vacuuming to make newly-freed @@ -1357,6 +1398,11 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats, RecordPageWithFreeSpace(onerel, blkno, freespace, nblocks); } + /* No dead tuples should be left if index cleanup is enabled */ + Assert((params->index_cleanup == VACOPT_TERNARY_ENABLED && + nleft_dead_tuples == 0 && nleft_dead_itemids == 0) || + params->index_cleanup == VACOPT_TERNARY_DISABLED); + /* report that everything is scanned and vacuumed */ pgstat_progress_update_param(PROGRESS_VACUUM_HEAP_BLKS_SCANNED, blkno); @@ -1364,7 +1410,9 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats, /* save stats for use later */ vacrelstats->tuples_deleted = tups_vacuumed; - vacrelstats->new_dead_tuples = nkeep; + vacrelstats->new_dead_tuples = nkeep + nleft_dead_tuples; + vacrelstats->nleft_dead_tuples = nleft_dead_tuples; + vacrelstats->nleft_dead_itemids = nleft_dead_itemids; /* now we can compute the new value for pg_class.reltuples */ vacrelstats->new_live_tuples = vac_estimate_reltuples(onerel, @@ -1433,8 +1481,11 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats, PROGRESS_VACUUM_PHASE_INDEX_CLEANUP); /* Do post-vacuum cleanup and statistics update for each index */ - for (i = 0; i < nindexes; i++) - lazy_cleanup_index(Irel[i], indstats[i], vacrelstats); + if (vacrelstats->useindex) + { + for (i = 0; i < nindexes; i++) + lazy_cleanup_index(Irel[i], indstats[i], vacrelstats); + } /* If no indexes, make log report that lazy_vacuum_heap would've made */ if (vacuumed_pages) @@ -1465,6 +1516,8 @@ lazy_scan_heap(Relation onerel, int options, LVRelStats *vacrelstats, "%u pages are entirely empty.\n", empty_pages), empty_pages); + appendStringInfo(&buf, "%.0f tuples and %.0f item identifiers are left as dead.\n", + nleft_dead_tuples, nleft_dead_itemids); appendStringInfo(&buf, _("%s."), pg_rusage_show(&ru0)); ereport(elevel, @@ -2110,7 +2163,7 @@ lazy_space_alloc(LVRelStats *vacrelstats, BlockNumber relblocks) autovacuum_work_mem != -1 ? autovacuum_work_mem : maintenance_work_mem; - if (vacrelstats->hasindex) + if (vacrelstats->useindex) { maxtuples = (vac_work_mem * 1024L) / sizeof(ItemPointerData); maxtuples = Min(maxtuples, INT_MAX); diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c index fd2e47ffc4..1a7291d94b 100644 --- a/src/backend/commands/vacuum.c +++ b/src/backend/commands/vacuum.c @@ -76,6 +76,7 @@ static void vac_truncate_clog(TransactionId frozenXID, TransactionId lastSaneFrozenXid, MultiXactId lastSaneMinMulti); static bool vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params); +static VacOptTernaryValue get_vacopt_ternary_value(DefElem *def); /* * Primary entry point for manual VACUUM and ANALYZE commands @@ -95,6 +96,9 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel) bool disable_page_skipping = false; ListCell *lc; + /* Set default value */ + params.index_cleanup = VACOPT_TERNARY_DEFAULT; + /* Parse options list */ foreach(lc, vacstmt->options) { @@ -120,6 +124,8 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel) full = defGetBoolean(opt); else if (strcmp(opt->defname, "disable_page_skipping") == 0) disable_page_skipping = defGetBoolean(opt); + else if (strcmp(opt->defname, "index_cleanup") == 0) + params.index_cleanup = get_vacopt_ternary_value(opt); else ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), @@ -1719,6 +1725,16 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params) onerelid = onerel->rd_lockInfo.lockRelId; LockRelationIdForSession(&onerelid, lmode); + /* Set index cleanup option based on reloptions if not yet */ + if (params->index_cleanup == VACOPT_TERNARY_DEFAULT) + { + if (onerel->rd_options == NULL || + ((StdRdOptions *) onerel->rd_options)->vacuum_index_cleanup) + params->index_cleanup = VACOPT_TERNARY_ENABLED; + else + params->index_cleanup = VACOPT_TERNARY_DISABLED; + } + /* * Remember the relation's TOAST relation for later, if the caller asked * us to process it. In VACUUM FULL, though, the toast table is @@ -1899,3 +1915,15 @@ vacuum_delay_point(void) CHECK_FOR_INTERRUPTS(); } } + +/* + * A wrapper function of defGetBoolean(). + * + * This function returns VACOPT_TERNARY_ENABLED and VACOPT_TERNARY_DISABLED + * instead of true and false. + */ +static VacOptTernaryValue +get_vacopt_ternary_value(DefElem *def) +{ + return defGetBoolean(def) ? VACOPT_TERNARY_ENABLED : VACOPT_TERNARY_DISABLED; +} diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index fa875db816..0976029e73 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -2886,6 +2886,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map, (dovacuum ? VACOPT_VACUUM : 0) | (doanalyze ? VACOPT_ANALYZE : 0) | (!wraparound ? VACOPT_SKIP_LOCKED : 0); + tab->at_params.index_cleanup = VACOPT_TERNARY_DEFAULT; tab->at_params.freeze_min_age = freeze_min_age; tab->at_params.freeze_table_age = freeze_table_age; tab->at_params.multixact_freeze_min_age = multixact_freeze_min_age; diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index d34bf86fc2..22576adc51 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -1039,6 +1039,7 @@ static const char *const table_storage_parameters[] = { "toast.log_autovacuum_min_duration", "toast_tuple_target", "user_catalog_table", + "vacuum_index_cleanup", NULL }; @@ -3443,8 +3444,9 @@ 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"); - else if (TailMatches("FULL|FREEZE|ANALYZE|VERBOSE|DISABLE_PAGE_SKIPPING|SKIP_LOCKED")) + "DISABLE_PAGE_SKIPPING", "SKIP_LOCKED", + "INDEX_CLEANUP"); + else if (TailMatches("FULL|FREEZE|ANALYZE|VERBOSE|DISABLE_PAGE_SKIPPING|SKIP_LOCKED|INDEX_CLEANUP")) COMPLETE_WITH("ON", "OFF"); } else if (HeadMatches("VACUUM") && TailMatches("(")) diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h index 77086f3e91..9cc6e0d023 100644 --- a/src/include/commands/vacuum.h +++ b/src/include/commands/vacuum.h @@ -148,6 +148,19 @@ typedef enum VacuumOption VACOPT_DISABLE_PAGE_SKIPPING = 1 << 7 /* don't skip any pages */ } VacuumOption; +/* + * A ternary value used by vacuum parameters. + * + * DEFAULT value is used to determine the value based on other + * configurations, e.g. reloptions. + */ +typedef enum VacOptTernaryValue +{ + VACOPT_TERNARY_DEFAULT = 0, + VACOPT_TERNARY_DISABLED, + VACOPT_TERNARY_ENABLED, +} VacOptTernaryValue; + /* * Parameters customizing behavior of VACUUM and ANALYZE. * @@ -167,6 +180,8 @@ typedef struct VacuumParams int log_min_duration; /* minimum execution threshold in ms at * which verbose logs are activated, -1 * to use default */ + VacOptTernaryValue index_cleanup; /* Do index vacuum and cleanup, + * default value depends on reloptions */ } VacuumParams; /* GUC parameters */ diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h index 54028515a7..89a7fbf73a 100644 --- a/src/include/utils/rel.h +++ b/src/include/utils/rel.h @@ -266,6 +266,7 @@ typedef struct StdRdOptions AutoVacOpts autovacuum; /* autovacuum-related options */ bool user_catalog_table; /* use as an additional catalog relation */ int parallel_workers; /* max number of parallel workers */ + bool vacuum_index_cleanup; /* enables index vacuuming and cleanup */ } StdRdOptions; #define HEAP_MIN_FILLFACTOR 10 diff --git a/src/test/regress/expected/vacuum.out b/src/test/regress/expected/vacuum.out index 07d0703115..6ba7cd726b 100644 --- a/src/test/regress/expected/vacuum.out +++ b/src/test/regress/expected/vacuum.out @@ -80,6 +80,14 @@ CONTEXT: SQL function "do_analyze" statement 1 SQL function "wrap_do_analyze" statement 1 VACUUM FULL vactst; VACUUM (DISABLE_PAGE_SKIPPING) vaccluster; +-- INDEX_CLEANUP option +CREATE TABLE no_index_cleanup (i INT PRIMARY KEY) WITH (vacuum_index_cleanup = false); +VACUUM (INDEX_CLEANUP FALSE) vaccluster; +VACUUM (INDEX_CLEANUP FALSE) vactst; -- index cleanup option is ignored if no indexes +VACUUM (INDEX_CLEANUP FALSE, FREEZE TRUE) vaccluster; +-- index cleanup option is ignored if VACUUM FULL +VACUUM (INDEX_CLEANUP TRUE, FULL TRUE) no_index_cleanup; +VACUUM (FULL TRUE) no_index_cleanup; -- partitioned table CREATE TABLE vacparted (a int, b char) PARTITION BY LIST (a); CREATE TABLE vacparted1 PARTITION OF vacparted FOR VALUES IN (1); @@ -136,6 +144,7 @@ ANALYZE (SKIP_LOCKED) vactst; DROP TABLE vaccluster; DROP TABLE vactst; DROP TABLE vacparted; +DROP TABLE no_index_cleanup; -- relation ownership, WARNING logs generated as all are skipped. CREATE TABLE vacowned (a int); CREATE TABLE vacowned_parted (a int) PARTITION BY LIST (a); diff --git a/src/test/regress/sql/vacuum.sql b/src/test/regress/sql/vacuum.sql index 81f3822679..57e0f354dd 100644 --- a/src/test/regress/sql/vacuum.sql +++ b/src/test/regress/sql/vacuum.sql @@ -62,6 +62,15 @@ VACUUM FULL vactst; VACUUM (DISABLE_PAGE_SKIPPING) vaccluster; +-- INDEX_CLEANUP option +CREATE TABLE no_index_cleanup (i INT PRIMARY KEY) WITH (vacuum_index_cleanup = false); +VACUUM (INDEX_CLEANUP FALSE) vaccluster; +VACUUM (INDEX_CLEANUP FALSE) vactst; -- index cleanup option is ignored if no indexes +VACUUM (INDEX_CLEANUP FALSE, FREEZE TRUE) vaccluster; +-- index cleanup option is ignored if VACUUM FULL +VACUUM (INDEX_CLEANUP TRUE, FULL TRUE) no_index_cleanup; +VACUUM (FULL TRUE) no_index_cleanup; + -- partitioned table CREATE TABLE vacparted (a int, b char) PARTITION BY LIST (a); CREATE TABLE vacparted1 PARTITION OF vacparted FOR VALUES IN (1); @@ -107,6 +116,7 @@ ANALYZE (SKIP_LOCKED) vactst; DROP TABLE vaccluster; DROP TABLE vactst; DROP TABLE vacparted; +DROP TABLE no_index_cleanup; -- relation ownership, WARNING logs generated as all are skipped. CREATE TABLE vacowned (a int);