*** a/doc/src/sgml/ref/create_table.sgml --- b/doc/src/sgml/ref/create_table.sgml *************** *** 1404,1409 **** WITH ( MODULUS numeric_literal, REM --- 1404,1423 ---- + + vacuum_truncate_enabled, toast.vacuum_truncate_enabled (boolean) + + + Enables vacuum to try to truncate off any empty pages at the end + of this table. The default value is true. + If true, the disk space for the truncated pages + is returned to the operating system, but note that + the truncation requires ACCESS EXCLUSIVE lock + on the table. + + + + autovacuum_vacuum_threshold, toast.autovacuum_vacuum_threshold (integer) *** a/src/backend/access/common/reloptions.c --- b/src/backend/access/common/reloptions.c *************** *** 89,94 **** --- 89,99 ---- * Setting parallel_workers is safe, since it acts the same as * max_parallel_workers_per_gather which is a USERSET parameter that doesn't * affect existing plans or queries. + * + * vacuum_truncate_enabled can be set at ShareUpdateExclusiveLock because it + * is only used during VACUUM, which uses a ShareUpdateExclusiveLock, + * so the VACUUM will not be affected by in-flight changes. Changing its + * value has no affect until the next VACUUM, so no need for stronger lock. */ static relopt_bool boolRelOpts[] = *************** *** 147,152 **** static relopt_bool boolRelOpts[] = --- 152,166 ---- }, true }, + { + { + "vacuum_truncate_enabled", + "Enables vacuum to truncate empty pages at the end of this table", + RELOPT_KIND_HEAP | RELOPT_KIND_TOAST, + ShareUpdateExclusiveLock + }, + true + }, /* list terminator */ {{NULL}} }; *************** *** 1399,1405 **** default_reloptions(Datum reloptions, bool validate, relopt_kind kind) {"vacuum_cleanup_index_scale_factor", RELOPT_TYPE_REAL, offsetof(StdRdOptions, vacuum_cleanup_index_scale_factor)}, {"vacuum_index_cleanup", RELOPT_TYPE_BOOL, ! offsetof(StdRdOptions, vacuum_index_cleanup)} }; options = parseRelOptions(reloptions, validate, kind, &numoptions); --- 1413,1421 ---- {"vacuum_cleanup_index_scale_factor", RELOPT_TYPE_REAL, offsetof(StdRdOptions, vacuum_cleanup_index_scale_factor)}, {"vacuum_index_cleanup", RELOPT_TYPE_BOOL, ! offsetof(StdRdOptions, vacuum_index_cleanup)}, ! {"vacuum_truncate_enabled", RELOPT_TYPE_BOOL, ! offsetof(StdRdOptions, vacuum_truncate_enabled)} }; options = parseRelOptions(reloptions, validate, kind, &numoptions); *** a/src/backend/access/heap/vacuumlazy.c --- b/src/backend/access/heap/vacuumlazy.c *************** *** 165,171 **** static void lazy_cleanup_index(Relation indrel, LVRelStats *vacrelstats); static int lazy_vacuum_page(Relation onerel, BlockNumber blkno, Buffer buffer, int tupindex, LVRelStats *vacrelstats, Buffer *vmbuffer); ! static bool should_attempt_truncation(LVRelStats *vacrelstats); static void lazy_truncate_heap(Relation onerel, LVRelStats *vacrelstats); static BlockNumber count_nondeletable_pages(Relation onerel, LVRelStats *vacrelstats); --- 165,171 ---- LVRelStats *vacrelstats); static int lazy_vacuum_page(Relation onerel, BlockNumber blkno, Buffer buffer, int tupindex, LVRelStats *vacrelstats, Buffer *vmbuffer); ! static bool should_attempt_truncation(Relation rel, LVRelStats *vacrelstats); static void lazy_truncate_heap(Relation onerel, LVRelStats *vacrelstats); static BlockNumber count_nondeletable_pages(Relation onerel, LVRelStats *vacrelstats); *************** *** 306,312 **** heap_vacuum_rel(Relation onerel, VacuumParams *params, /* * Optionally truncate the relation. */ ! if (should_attempt_truncation(vacrelstats)) lazy_truncate_heap(onerel, vacrelstats); /* Report that we are now doing final cleanup */ --- 306,312 ---- /* * Optionally truncate the relation. */ ! if (should_attempt_truncation(onerel, vacrelstats)) lazy_truncate_heap(onerel, vacrelstats); /* Report that we are now doing final cleanup */ *************** *** 660,666 **** lazy_scan_heap(Relation onerel, VacuumParams *params, LVRelStats *vacrelstats, /* see note above about forcing scanning of last page */ #define FORCE_CHECK_PAGE() \ ! (blkno == nblocks - 1 && should_attempt_truncation(vacrelstats)) pgstat_progress_update_param(PROGRESS_VACUUM_HEAP_BLKS_SCANNED, blkno); --- 660,666 ---- /* see note above about forcing scanning of last page */ #define FORCE_CHECK_PAGE() \ ! (blkno == nblocks - 1 && should_attempt_truncation(onerel, vacrelstats)) pgstat_progress_update_param(PROGRESS_VACUUM_HEAP_BLKS_SCANNED, blkno); *************** *** 1869,1878 **** lazy_cleanup_index(Relation indrel, * careful to depend only on fields that lazy_scan_heap updates on-the-fly. */ static bool ! should_attempt_truncation(LVRelStats *vacrelstats) { BlockNumber possibly_freeable; possibly_freeable = vacrelstats->rel_pages - vacrelstats->nonempty_pages; if (possibly_freeable > 0 && (possibly_freeable >= REL_TRUNCATE_MINIMUM || --- 1869,1882 ---- * careful to depend only on fields that lazy_scan_heap updates on-the-fly. */ static bool ! should_attempt_truncation(Relation rel, LVRelStats *vacrelstats) { BlockNumber possibly_freeable; + if (rel->rd_options != NULL && + ((StdRdOptions *) rel->rd_options)->vacuum_truncate_enabled == false) + return false; + possibly_freeable = vacrelstats->rel_pages - vacrelstats->nonempty_pages; if (possibly_freeable > 0 && (possibly_freeable >= REL_TRUNCATE_MINIMUM || *** a/src/bin/psql/tab-complete.c --- b/src/bin/psql/tab-complete.c *************** *** 1056,1064 **** static const char *const table_storage_parameters[] = { --- 1056,1066 ---- "toast.autovacuum_vacuum_scale_factor", "toast.autovacuum_vacuum_threshold", "toast.log_autovacuum_min_duration", + "toast.vacuum_truncate_enabled", "toast_tuple_target", "user_catalog_table", "vacuum_index_cleanup", + "vacuum_truncate_enabled", NULL }; *** a/src/include/utils/rel.h --- b/src/include/utils/rel.h *************** *** 267,272 **** typedef struct StdRdOptions --- 267,274 ---- 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 */ + bool vacuum_truncate_enabled; /* enables vacuum to truncate + * a relation */ } StdRdOptions; #define HEAP_MIN_FILLFACTOR 10 *** a/src/test/regress/expected/reloptions.out --- b/src/test/regress/expected/reloptions.out *************** *** 87,92 **** SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass AND --- 87,139 ---- -- RESET fails if a value is specified ALTER TABLE reloptions_test RESET (fillfactor=12); ERROR: RESET must not include values for parameters + -- Test vacuum_truncate_enabled option + DROP TABLE reloptions_test; + CREATE TABLE reloptions_test(i INT NOT NULL, j text) + WITH (vacuum_truncate_enabled=false, + toast.vacuum_truncate_enabled=false, + autovacuum_enabled=false); + SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass; + reloptions + ---------------------------------------------------------- + {vacuum_truncate_enabled=false,autovacuum_enabled=false} + (1 row) + + INSERT INTO reloptions_test VALUES (1, NULL), (NULL, NULL); + ERROR: null value in column "i" violates not-null constraint + DETAIL: Failing row contains (null, null). + VACUUM reloptions_test; + SELECT pg_relation_size('reloptions_test') > 0; + ?column? + ---------- + t + (1 row) + + SELECT reloptions FROM pg_class WHERE oid = + (SELECT reltoastrelid FROM pg_class + WHERE oid = 'reloptions_test'::regclass); + reloptions + --------------------------------- + {vacuum_truncate_enabled=false} + (1 row) + + ALTER TABLE reloptions_test RESET (vacuum_truncate_enabled); + SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass; + reloptions + ---------------------------- + {autovacuum_enabled=false} + (1 row) + + INSERT INTO reloptions_test VALUES (1, NULL), (NULL, NULL); + ERROR: null value in column "i" violates not-null constraint + DETAIL: Failing row contains (null, null). + VACUUM reloptions_test; + SELECT pg_relation_size('reloptions_test') = 0; + ?column? + ---------- + t + (1 row) + -- Test toast.* options DROP TABLE reloptions_test; CREATE TABLE reloptions_test (s VARCHAR) *** a/src/test/regress/sql/reloptions.sql --- b/src/test/regress/sql/reloptions.sql *************** *** 52,57 **** SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass AND --- 52,79 ---- -- RESET fails if a value is specified ALTER TABLE reloptions_test RESET (fillfactor=12); + -- Test vacuum_truncate_enabled option + DROP TABLE reloptions_test; + + CREATE TABLE reloptions_test(i INT NOT NULL, j text) + WITH (vacuum_truncate_enabled=false, + toast.vacuum_truncate_enabled=false, + autovacuum_enabled=false); + SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass; + INSERT INTO reloptions_test VALUES (1, NULL), (NULL, NULL); + VACUUM reloptions_test; + SELECT pg_relation_size('reloptions_test') > 0; + + SELECT reloptions FROM pg_class WHERE oid = + (SELECT reltoastrelid FROM pg_class + WHERE oid = 'reloptions_test'::regclass); + + ALTER TABLE reloptions_test RESET (vacuum_truncate_enabled); + SELECT reloptions FROM pg_class WHERE oid = 'reloptions_test'::regclass; + INSERT INTO reloptions_test VALUES (1, NULL), (NULL, NULL); + VACUUM reloptions_test; + SELECT pg_relation_size('reloptions_test') = 0; + -- Test toast.* options DROP TABLE reloptions_test;