problems with toast.* reloptions
While investigating problems caused by vacuum_rel() scribbling on its
VacuumParams argument [0]/messages/by-id/flat/CAGRkXqTo+aK=GTy5pSc-9cy8H2F2TJvcrZ-zXEiNJj93np1UUw@mail.gmail.com, I noticed some other interesting bugs with the
toast.* reloption code. Note that the documentation for the reloptions has
the following line:
If a table parameter value is set and the equivalent toast. parameter
is not, the TOAST table will use the table's parameter value.
The problems I found are as follows:
* vacuum_rel() does not look up the main relation's reloptions when
processing a TOAST table, which is a problem for manual VACUUMs. The
aforementioned bug [0]/messages/by-id/flat/CAGRkXqTo+aK=GTy5pSc-9cy8H2F2TJvcrZ-zXEiNJj93np1UUw@mail.gmail.com causes you to sometimes get the expected behavior
(because the parameters are overridden before recursing to TOAST), but
fixing that bug makes that accidental behavior go away.
* For autovacuum, the main table's reloptions are only used if the TOAST
table has no reloptions set. So, if your relation has
autovacuum_vacuum_threshold and toast.vacuum_index_cleanup set, the main
relation's autovacuum_vacuum_threshold setting won't be used for the
TOAST table.
* Even when the preceding point doesn't apply, autovacuum doesn't use the
main relation's setting for some parameters (e.g., vacuum_truncate).
Instead, it leaves them uninitialized and expects vacuum_rel() to fill
them in. This is a problem because, as mentioned earlier, vacuum_rel()
doesn't consult the main relation's reloptions either.
I think we need to do something like the following to fix this:
* Teach autovacuum to combine the TOAST reloptions with the main relation's
when processing TOAST tables (with the toast.* ones winning if both are
set).
* Teach autovacuum to resolve reloptions for parameters like
vacuum_truncate instead of relying on vacuum_rel() to fill it in.
* Have vacuum_rel() send the main relation's reloptions when recursing to
the TOAST table so that we can combine them there, too.
This doesn't fix VACUUM against a TOAST table directly (e.g., VACUUM
pg_toast.pg_toast_5432), but that might not be too important because
(PROCESS_TOAST TRUE) is the main supported way to vacuum a TOAST table. If
we did want to fix that, though, I think we'd have to teach vacuum_rel() or
the relcache to look up the reloptions for the main relation.
Thoughts?
[0]: /messages/by-id/flat/CAGRkXqTo+aK=GTy5pSc-9cy8H2F2TJvcrZ-zXEiNJj93np1UUw@mail.gmail.com
--
nathan
On Thu, Jun 19, 2025 at 03:20:27PM -0500, Nathan Bossart wrote:
While investigating problems caused by vacuum_rel() scribbling on its
VacuumParams argument [0], I noticed some other interesting bugs with the
toast.* reloption code. Note that the documentation for the reloptions has
the following line:If a table parameter value is set and the equivalent toast. parameter
is not, the TOAST table will use the table's parameter value.The problems I found are as follows:
* vacuum_rel() does not look up the main relation's reloptions when
processing a TOAST table, which is a problem for manual VACUUMs. The
aforementioned bug [0] causes you to sometimes get the expected behavior
(because the parameters are overridden before recursing to TOAST), but
fixing that bug makes that accidental behavior go away.
Are you referring to the case of a VACUUM pg_toast.pg_toast_NNN? I'm
not sure that we really need to care about looking up at the parent
relation in this case. It sounds to me that the intention of this
paragraph is for the case where the TOAST table is treated as a
secondary table, not when the TOAST table is directly vacuumed.
Perhaps the wording of the docs should be improved that this does not
happen if vacuuming directly a TOAST table.
* For autovacuum, the main table's reloptions are only used if the TOAST
table has no reloptions set. So, if your relation has
autovacuum_vacuum_threshold and toast.vacuum_index_cleanup set, the main
relation's autovacuum_vacuum_threshold setting won't be used for the
TOAST table.
[.. /me double-checks the code .. ]
So we combine the options in do_autovacuum() with the two-pass logic
to gather the relation OIDs, then apply relation_needs_vacanalyze().
That looks like an old issue, that cannot be solved as long as we rely
on the relopts to be an all-or-nothing thing when assigning the
individual values for the TOAST relation. Oops.
* Even when the preceding point doesn't apply, autovacuum doesn't use the
main relation's setting for some parameters (e.g., vacuum_truncate).
Instead, it leaves them uninitialized and expects vacuum_rel() to fill
them in. This is a problem because, as mentioned earlier, vacuum_rel()
doesn't consult the main relation's reloptions either.
Relying on vacuum_rel() sounds like a bad idea if we can avoid that,
still perhaps that's OK as long as we don't use a pointer to the
VacuumParams and keep the updates to the value of vacuum_rel() local
inside the routine.
I think we need to do something like the following to fix this:
* Teach autovacuum to combine the TOAST reloptions with the main relation's
when processing TOAST tables (with the toast.* ones winning if both are
set).* Teach autovacuum to resolve reloptions for parameters like
vacuum_truncate instead of relying on vacuum_rel() to fill it in.
These two points make sense here, yes.
* Have vacuum_rel() send the main relation's reloptions when recursing to
the TOAST table so that we can combine them there, too.
For the case of a manual VACUUM on the main table, where the TOAST
table is treated as a secondary citizen, that makes sense as well,
yes.
This doesn't fix VACUUM against a TOAST table directly (e.g., VACUUM
pg_toast.pg_toast_5432), but that might not be too important because
(PROCESS_TOAST TRUE) is the main supported way to vacuum a TOAST table. If
we did want to fix that, though, I think we'd have to teach vacuum_rel() or
the relcache to look up the reloptions for the main relation.
This one does not sound that important to me for the case of manual
VACUUM case directly done on a TOAST table. If you do that, the code
kind of assumes that a TOAST table is actually a "main" relation that
has no TOAST table. That should keep the code simpler, because we
would not need to look at what the parent relation holds when deciding
which options to use in ExecVacuum(). The autovacuum case is
different, as TOAST relations are worked on as their own items rather
than being secondary relations of the main tables.
--
Michael
On Fri, Jun 20, 2025 at 11:05:37AM +0900, Michael Paquier wrote:
On Thu, Jun 19, 2025 at 03:20:27PM -0500, Nathan Bossart wrote:
* vacuum_rel() does not look up the main relation's reloptions when
processing a TOAST table, which is a problem for manual VACUUMs. The
aforementioned bug [0] causes you to sometimes get the expected behavior
(because the parameters are overridden before recursing to TOAST), but
fixing that bug makes that accidental behavior go away.Are you referring to the case of a VACUUM pg_toast.pg_toast_NNN? I'm
not sure that we really need to care about looking up at the parent
relation in this case. It sounds to me that the intention of this
paragraph is for the case where the TOAST table is treated as a
secondary table, not when the TOAST table is directly vacuumed.
Perhaps the wording of the docs should be improved that this does not
happen if vacuuming directly a TOAST table.
Yeah, I was mainly thinking of a VACUUM command that recurses to the TOAST
table. Of course, it'd be nice to fix VACUUM pg_toast.pg_toast_NNN, too,
but I'm personally not too worried about that use-case.
This doesn't fix VACUUM against a TOAST table directly (e.g., VACUUM
pg_toast.pg_toast_5432), but that might not be too important because
(PROCESS_TOAST TRUE) is the main supported way to vacuum a TOAST table. If
we did want to fix that, though, I think we'd have to teach vacuum_rel() or
the relcache to look up the reloptions for the main relation.This one does not sound that important to me for the case of manual
VACUUM case directly done on a TOAST table. If you do that, the code
kind of assumes that a TOAST table is actually a "main" relation that
has no TOAST table. That should keep the code simpler, because we
would not need to look at what the parent relation holds when deciding
which options to use in ExecVacuum(). The autovacuum case is
different, as TOAST relations are worked on as their own items rather
than being secondary relations of the main tables.
+1
--
nathan
I think we need to do something like the following to fix this:
* Teach autovacuum to combine the TOAST reloptions with the main relation's
when processing TOAST tables (with the toast.* ones winning if both are
set).* Teach autovacuum to resolve reloptions for parameters like
vacuum_truncate instead of relying on vacuum_rel() to fill it in.
These two points make sense here, yes.
I investigated that this afternoon and identified two potential
implementation approaches:
1) Create functions like resolve_toast_vac_opts() and
resolve_toast_rel_opts(). These would then be used in
table_recheck_autovac(), NeedsAutoVacTableForXidWraparound(), and
do_autovacuum() after the toast table check.
2) When updating a table's relopt, also update the relopt of its
associated TOAST table if it's not already set. Similarly, when
creating a new TOAST table, it would inherit the parent's relopt.
Option 2 seems more reasonable to me, as it avoids requiring customers
to manually resolve these options, when they have different settings
for the parent and TOAST tables."
On Sat, Jun 21, 2025 at 11:45:25PM -0400, shihao zhong wrote:
2) When updating a table's relopt, also update the relopt of its
associated TOAST table if it's not already set. Similarly, when
creating a new TOAST table, it would inherit the parent's relopt.Option 2 seems more reasonable to me, as it avoids requiring customers
to manually resolve these options, when they have different settings
for the parent and TOAST tables."
I like this one, but since it won't fix existing clusters, it might only be
workable for v19.
--
nathan
On Fri, Jun 20, 2025 at 11:05:37AM +0900, Michael Paquier wrote:
On Thu, Jun 19, 2025 at 03:20:27PM -0500, Nathan Bossart wrote:
I think we need to do something like the following to fix this:
* Teach autovacuum to combine the TOAST reloptions with the main relation's
when processing TOAST tables (with the toast.* ones winning if both are
set).* Teach autovacuum to resolve reloptions for parameters like
vacuum_truncate instead of relying on vacuum_rel() to fill it in.These two points make sense here, yes.
* Have vacuum_rel() send the main relation's reloptions when recursing to
the TOAST table so that we can combine them there, too.For the case of a manual VACUUM on the main table, where the TOAST
table is treated as a secondary citizen, that makes sense as well,
yes.
Here is a very rough proof-of-concept patch set for this. AFAICT there are
a few options we cannot fix on the back-branches because there is no way to
tell whether it is set or has just picked up the default. On v18 and
newer, we could use isset_offset, but that doesn't exist on older versions.
(I haven't looked closely, but I'm assuming that back-patching isset_offset
isn't an option.)
I would like to explore the "option 2" from upthread [0]/messages/by-id/aFl598epAdUrrv0y@nathan for v19. I think
that is a better long-term solution, and it may allow us to remove the
table_toast_map in autovacuum.
[0]: /messages/by-id/aFl598epAdUrrv0y@nathan
--
nathan
Attachments:
v1-0003-autovac-combine-reloptions-correctly.patchtext/plain; charset=us-asciiDownload
From 12b6404c906837c9244110095ca980ad3545bc0b Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 23 Jun 2025 15:16:38 -0500
Subject: [PATCH v1 3/4] autovac: combine reloptions correctly
---
src/backend/postmaster/autovacuum.c | 85 +++++++++++++++++++++++++++++
1 file changed, 85 insertions(+)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 3bedca971ff..0d1f4e38b58 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -1873,6 +1873,73 @@ get_database_list(void)
return dblist;
}
+static void
+combine_relopts(StdRdOptions *toast_opts, const StdRdOptions *main_opts)
+{
+ AutoVacOpts *toast_avopts = &toast_opts->autovacuum;
+ const AutoVacOpts *main_avopts = &main_opts->autovacuum;
+
+ /* XXX: need isset_offset to combine "enabled" */
+
+ if (toast_avopts->vacuum_threshold == -1)
+ toast_avopts->vacuum_threshold = main_avopts->vacuum_threshold;
+
+ if (toast_avopts->vacuum_max_threshold == -2)
+ toast_avopts->vacuum_max_threshold = main_avopts->vacuum_max_threshold;
+
+ if (toast_avopts->vacuum_ins_threshold == -2)
+ toast_avopts->vacuum_ins_threshold = main_avopts->vacuum_ins_threshold;
+
+ if (toast_avopts->analyze_threshold == -1)
+ toast_avopts->analyze_threshold = main_avopts->analyze_threshold;
+
+ if (toast_avopts->vacuum_cost_limit == -1)
+ toast_avopts->vacuum_cost_limit = main_avopts->vacuum_cost_limit;
+
+ if (toast_avopts->freeze_min_age == -1)
+ toast_avopts->freeze_min_age = main_avopts->freeze_min_age;
+
+ if (toast_avopts->freeze_max_age == -1)
+ toast_avopts->freeze_max_age = main_avopts->freeze_max_age;
+
+ if (toast_avopts->freeze_table_age == -1)
+ toast_avopts->freeze_table_age = main_avopts->freeze_table_age;
+
+ if (toast_avopts->multixact_freeze_min_age == -1)
+ toast_avopts->multixact_freeze_min_age = main_avopts->multixact_freeze_min_age;
+
+ if (toast_avopts->multixact_freeze_max_age == -1)
+ toast_avopts->multixact_freeze_max_age = main_avopts->multixact_freeze_max_age;
+
+ if (toast_avopts->multixact_freeze_table_age == -1)
+ toast_avopts->multixact_freeze_table_age = main_avopts->multixact_freeze_table_age;
+
+ /* XXX: need isset_offset for log_min_duration */
+
+ if (toast_avopts->vacuum_cost_delay == -1)
+ toast_avopts->vacuum_cost_delay = main_avopts->vacuum_cost_delay;
+
+ if (toast_avopts->vacuum_scale_factor == -1)
+ toast_avopts->vacuum_scale_factor = main_avopts->vacuum_scale_factor;
+
+ if (toast_avopts->vacuum_ins_scale_factor == -1)
+ toast_avopts->vacuum_ins_scale_factor = main_avopts->vacuum_ins_scale_factor;
+
+ if (toast_avopts->analyze_scale_factor == -1)
+ toast_avopts->analyze_scale_factor = main_avopts->analyze_scale_factor;
+
+ /* XXX: need isset_offset for vacuum_index_cleanup */
+
+ if (!toast_opts->vacuum_truncate_set && main_opts->vacuum_truncate_set)
+ {
+ toast_opts->vacuum_truncate = main_opts->vacuum_truncate;
+ toast_opts->vacuum_truncate_set = true;
+ }
+
+ if (toast_opts->vacuum_max_eager_freeze_failure_rate == -1)
+ toast_opts->vacuum_max_eager_freeze_failure_rate = main_opts->vacuum_max_eager_freeze_failure_rate;
+}
+
/*
* Process a database table-by-table
*
@@ -2113,7 +2180,16 @@ do_autovacuum(void)
*/
relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
if (relopts)
+ {
+ av_relation *hentry;
+ bool found;
+
free_relopts = true;
+
+ hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
+ if (found && hentry->ar_hasrelopts)
+ combine_relopts(relopts, &hentry->ar_reloptions);
+ }
else
{
av_relation *hentry;
@@ -2733,7 +2809,16 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
*/
relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
if (relopts)
+ {
+ av_relation *hentry;
+ bool found;
+
free_relopts = true;
+
+ hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
+ if (found && hentry->ar_hasrelopts)
+ combine_relopts(relopts, &hentry->ar_reloptions);
+ }
else if (classForm->relkind == RELKIND_TOASTVALUE &&
table_toast_map != NULL)
{
--
2.39.5 (Apple Git-154)
v1-0004-combine-relopts-correctly-for-VACUUM-commands.patchtext/plain; charset=us-asciiDownload
From 724f90f4d19a2c92026adffcde8bf47967378f1e Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 23 Jun 2025 15:40:28 -0500
Subject: [PATCH v1 4/4] combine relopts correctly for VACUUM commands
---
src/backend/commands/vacuum.c | 30 +++++++++++++++++++----------
src/backend/postmaster/autovacuum.c | 2 +-
src/include/commands/vacuum.h | 2 ++
3 files changed, 23 insertions(+), 11 deletions(-)
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 0015b9ef4b0..a74c3a9e2a2 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -124,7 +124,7 @@ static void vac_truncate_clog(TransactionId frozenXID,
TransactionId lastSaneFrozenXid,
MultiXactId lastSaneMinMulti);
static bool vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
- BufferAccessStrategy bstrategy);
+ BufferAccessStrategy bstrategy, StdRdOptions *main_opts);
static double compute_parallel_delay(void);
static VacOptValue get_vacoptval_from_boolean(DefElem *def);
static bool vac_tid_reaped(ItemPointer itemptr, void *state);
@@ -634,7 +634,7 @@ vacuum(List *relations, VacuumParams *params, BufferAccessStrategy bstrategy,
if (params->options & VACOPT_VACUUM)
{
- if (!vacuum_rel(vrel->oid, vrel->relation, params, bstrategy))
+ if (!vacuum_rel(vrel->oid, vrel->relation, params, bstrategy, NULL))
continue;
}
@@ -1998,7 +1998,7 @@ vac_truncate_clog(TransactionId frozenXID,
*/
static bool
vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
- BufferAccessStrategy bstrategy)
+ BufferAccessStrategy bstrategy, StdRdOptions *main_opts)
{
LOCKMODE lmode;
Relation rel;
@@ -2008,6 +2008,8 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
Oid save_userid;
int save_sec_context;
int save_nestlevel;
+ StdRdOptions saved_opts;
+ StdRdOptions combined_opts;
Assert(params != NULL);
@@ -2165,6 +2167,15 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
lockrelid = rel->rd_lockInfo.lockRelId;
LockRelationIdForSession(&lockrelid, lmode);
+ if (rel->rd_options)
+ {
+ memcpy(&saved_opts, rel->rd_options, sizeof(StdRdOptions));
+ memcpy(&combined_opts, rel->rd_options, sizeof(StdRdOptions));
+
+ if (main_opts)
+ combine_relopts(&combined_opts, main_opts);
+ }
+
/*
* Set index_cleanup option based on index_cleanup reloption if it wasn't
* specified in VACUUM command, or when running in an autovacuum worker
@@ -2176,8 +2187,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
if (rel->rd_options == NULL)
vacuum_index_cleanup = STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO;
else
- vacuum_index_cleanup =
- ((StdRdOptions *) rel->rd_options)->vacuum_index_cleanup;
+ vacuum_index_cleanup = combined_opts.vacuum_index_cleanup;
if (vacuum_index_cleanup == STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO)
params->index_cleanup = VACOPTVALUE_AUTO;
@@ -2197,9 +2207,9 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
*/
if (params->max_eager_freeze_failure_rate < 0 &&
rel->rd_options != NULL &&
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate >= 0)
+ combined_opts.vacuum_max_eager_freeze_failure_rate >= 0)
params->max_eager_freeze_failure_rate =
- ((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate;
+ combined_opts.vacuum_max_eager_freeze_failure_rate;
else
params->max_eager_freeze_failure_rate = vacuum_max_eager_freeze_failure_rate;
@@ -2211,9 +2221,9 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
{
StdRdOptions *opts = (StdRdOptions *) rel->rd_options;
- if (opts && opts->vacuum_truncate_set)
+ if (opts && combined_opts.vacuum_truncate_set)
{
- if (opts->vacuum_truncate)
+ if (combined_opts.vacuum_truncate)
params->truncate = VACOPTVALUE_ENABLED;
else
params->truncate = VACOPTVALUE_DISABLED;
@@ -2314,7 +2324,7 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
toast_vacuum_params.options |= VACOPT_PROCESS_MAIN;
toast_vacuum_params.toast_parent = relid;
- vacuum_rel(toast_relid, NULL, &toast_vacuum_params, bstrategy);
+ vacuum_rel(toast_relid, NULL, &toast_vacuum_params, bstrategy, &saved_opts);
}
/*
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 0d1f4e38b58..ca399b6a92e 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -1873,7 +1873,7 @@ get_database_list(void)
return dblist;
}
-static void
+void
combine_relopts(StdRdOptions *toast_opts, const StdRdOptions *main_opts)
{
AutoVacOpts *toast_avopts = &toast_opts->autovacuum;
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index bc37a80dc74..928f864581b 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -24,6 +24,7 @@
#include "parser/parse_node.h"
#include "storage/buf.h"
#include "storage/lock.h"
+#include "utils/rel.h"
#include "utils/relcache.h"
/*
@@ -377,6 +378,7 @@ extern IndexBulkDeleteResult *vac_cleanup_one_index(IndexVacuumInfo *ivinfo,
/* In postmaster/autovacuum.c */
extern void AutoVacuumUpdateCostLimit(void);
extern void VacuumUpdateCosts(void);
+extern void combine_relopts(StdRdOptions *toast_opts, const StdRdOptions *main_opts);
/* in commands/vacuumparallel.c */
extern ParallelVacuumState *parallel_vacuum_init(Relation rel, Relation *indrels,
--
2.39.5 (Apple Git-154)
v1-0001-autovac-save-all-relopts-instead-of-just-avopts.patchtext/plain; charset=us-asciiDownload
From a1930aa769a96c38621d7caa727c404d553b7ecc Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 23 Jun 2025 14:07:30 -0500
Subject: [PATCH v1 1/4] autovac: save all relopts instead of just avopts
---
src/backend/postmaster/autovacuum.c | 119 ++++++++++------------------
1 file changed, 43 insertions(+), 76 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 451fb90a610..f86c9fed853 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -192,8 +192,8 @@ typedef struct av_relation
Oid ar_toastrelid; /* hash key - must be first */
Oid ar_relid;
bool ar_hasrelopts;
- AutoVacOpts ar_reloptions; /* copy of AutoVacOpts from the main table's
- * reloptions, or NULL if none */
+ StdRdOptions ar_reloptions; /* copy of main table's reloptions, or NULL if
+ * none */
} av_relation;
/* struct to keep track of tables to vacuum and/or analyze, after rechecking */
@@ -333,11 +333,11 @@ static void FreeWorkerInfo(int code, Datum arg);
static autovac_table *table_recheck_autovac(Oid relid, HTAB *table_toast_map,
TupleDesc pg_class_desc,
int effective_multixact_freeze_max_age);
-static void recheck_relation_needs_vacanalyze(Oid relid, AutoVacOpts *avopts,
+static void recheck_relation_needs_vacanalyze(Oid relid, StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
bool *dovacuum, bool *doanalyze, bool *wraparound);
-static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
+static void relation_needs_vacanalyze(Oid relid, StdRdOptions *relopts,
Form_pg_class classForm,
PgStat_StatTabEntry *tabentry,
int effective_multixact_freeze_max_age,
@@ -345,8 +345,6 @@ static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts,
static void autovacuum_do_vac_analyze(autovac_table *tab,
BufferAccessStrategy bstrategy);
-static AutoVacOpts *extract_autovac_opts(HeapTuple tup,
- TupleDesc pg_class_desc);
static void perform_work_item(AutoVacuumWorkItem *workitem);
static void autovac_report_activity(autovac_table *tab);
static void autovac_report_workitem(AutoVacuumWorkItem *workitem,
@@ -1995,7 +1993,7 @@ do_autovacuum(void)
{
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
PgStat_StatTabEntry *tabentry;
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
Oid relid;
bool dovacuum;
bool doanalyze;
@@ -2033,7 +2031,7 @@ do_autovacuum(void)
}
/* Fetch reloptions and the pgstat entry for this table */
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
tabentry = pgstat_fetch_stat_tabentry_ext(classForm->relisshared,
relid);
@@ -2069,7 +2067,7 @@ do_autovacuum(void)
{
hentry->ar_hasrelopts = true;
memcpy(&hentry->ar_reloptions, relopts,
- sizeof(AutoVacOpts));
+ sizeof(StdRdOptions));
}
}
}
@@ -2095,7 +2093,7 @@ do_autovacuum(void)
Form_pg_class classForm = (Form_pg_class) GETSTRUCT(tuple);
PgStat_StatTabEntry *tabentry;
Oid relid;
- AutoVacOpts *relopts;
+ StdRdOptions *relopts;
bool free_relopts = false;
bool dovacuum;
bool doanalyze;
@@ -2113,7 +2111,7 @@ do_autovacuum(void)
* fetch reloptions -- if this toast table does not have them, try the
* main rel
*/
- relopts = extract_autovac_opts(tuple, pg_class_desc);
+ relopts = (StdRdOptions *) extractRelOptions(tuple, pg_class_desc, NULL);
if (relopts)
free_relopts = true;
else
@@ -2701,39 +2699,6 @@ deleted2:
pfree(cur_relname);
}
-/*
- * extract_autovac_opts
- *
- * Given a relation's pg_class tuple, return a palloc'd copy of the
- * AutoVacOpts portion of reloptions, if set; otherwise, return NULL.
- *
- * Note: callers do not have a relation lock on the table at this point,
- * so the table could have been dropped, and its catalog rows gone, after
- * we acquired the pg_class row. If pg_class had a TOAST table, this would
- * be a risk; fortunately, it doesn't.
- */
-static AutoVacOpts *
-extract_autovac_opts(HeapTuple tup, TupleDesc pg_class_desc)
-{
- bytea *relopts;
- AutoVacOpts *av;
-
- Assert(((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_RELATION ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_MATVIEW ||
- ((Form_pg_class) GETSTRUCT(tup))->relkind == RELKIND_TOASTVALUE);
-
- relopts = extractRelOptions(tup, pg_class_desc, NULL);
- if (relopts == NULL)
- return NULL;
-
- av = palloc(sizeof(AutoVacOpts));
- memcpy(av, &(((StdRdOptions *) relopts)->autovacuum), sizeof(AutoVacOpts));
- pfree(relopts);
-
- return av;
-}
-
-
/*
* table_recheck_autovac
*
@@ -2753,8 +2718,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
bool doanalyze;
autovac_table *tab = NULL;
bool wraparound;
- AutoVacOpts *avopts;
- bool free_avopts = false;
+ StdRdOptions *relopts;
+ bool free_relopts = false;
/* fetch the relation's relcache entry */
classTup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
@@ -2766,9 +2731,9 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
* Get the applicable reloptions. If it is a TOAST table, try to get the
* main table reloptions if the toast table itself doesn't have.
*/
- avopts = extract_autovac_opts(classTup, pg_class_desc);
- if (avopts)
- free_avopts = true;
+ relopts = (StdRdOptions *) extractRelOptions(classTup, pg_class_desc, NULL);
+ if (relopts)
+ free_relopts = true;
else if (classForm->relkind == RELKIND_TOASTVALUE &&
table_toast_map != NULL)
{
@@ -2777,10 +2742,10 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
hentry = hash_search(table_toast_map, &relid, HASH_FIND, &found);
if (found && hentry->ar_hasrelopts)
- avopts = &hentry->ar_reloptions;
+ relopts = &hentry->ar_reloptions;
}
- recheck_relation_needs_vacanalyze(relid, avopts, classForm,
+ recheck_relation_needs_vacanalyze(relid, relopts, classForm,
effective_multixact_freeze_max_age,
&dovacuum, &doanalyze, &wraparound);
@@ -2792,6 +2757,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
int multixact_freeze_min_age;
int multixact_freeze_table_age;
int log_min_duration;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/*
* Calculate the vacuum cost parameters and the freeze ages. If there
@@ -2879,8 +2845,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
avopts->vacuum_cost_delay >= 0));
}
- if (free_avopts)
- pfree(avopts);
+ if (free_relopts)
+ pfree(relopts);
heap_freetuple(classTup);
return tab;
}
@@ -2895,7 +2861,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
*/
static void
recheck_relation_needs_vacanalyze(Oid relid,
- AutoVacOpts *avopts,
+ StdRdOptions *relopts,
Form_pg_class classForm,
int effective_multixact_freeze_max_age,
bool *dovacuum,
@@ -2908,7 +2874,7 @@ recheck_relation_needs_vacanalyze(Oid relid,
tabentry = pgstat_fetch_stat_tabentry_ext(classForm->relisshared,
relid);
- relation_needs_vacanalyze(relid, avopts, classForm, tabentry,
+ relation_needs_vacanalyze(relid, relopts, classForm, tabentry,
effective_multixact_freeze_max_age,
dovacuum, doanalyze, wraparound);
@@ -2928,7 +2894,7 @@ recheck_relation_needs_vacanalyze(Oid relid,
* "dovacuum" and "doanalyze", respectively. Also return whether the vacuum is
* being forced because of Xid or multixact wraparound.
*
- * relopts is a pointer to the AutoVacOpts options (either for itself in the
+ * relopts is a pointer to the StdRdOptions options (either for itself in the
* case of a plain table, or for either itself or its parent table in the case
* of a TOAST table), NULL if none; tabentry is the pgstats entry, which can be
* NULL.
@@ -2962,7 +2928,7 @@ recheck_relation_needs_vacanalyze(Oid relid,
*/
static void
relation_needs_vacanalyze(Oid relid,
- AutoVacOpts *relopts,
+ StdRdOptions *relopts,
Form_pg_class classForm,
PgStat_StatTabEntry *tabentry,
int effective_multixact_freeze_max_age,
@@ -2973,6 +2939,7 @@ relation_needs_vacanalyze(Oid relid,
{
bool force_vacuum;
bool av_enabled;
+ AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
/* constants from reloptions or GUC variables */
int vac_base_thresh,
@@ -3010,45 +2977,45 @@ relation_needs_vacanalyze(Oid relid,
*/
/* -1 in autovac setting means use plain vacuum_scale_factor */
- vac_scale_factor = (relopts && relopts->vacuum_scale_factor >= 0)
- ? relopts->vacuum_scale_factor
+ vac_scale_factor = (avopts && avopts->vacuum_scale_factor >= 0)
+ ? avopts->vacuum_scale_factor
: autovacuum_vac_scale;
- vac_base_thresh = (relopts && relopts->vacuum_threshold >= 0)
- ? relopts->vacuum_threshold
+ vac_base_thresh = (avopts && avopts->vacuum_threshold >= 0)
+ ? avopts->vacuum_threshold
: autovacuum_vac_thresh;
/* -1 is used to disable max threshold */
- vac_max_thresh = (relopts && relopts->vacuum_max_threshold >= -1)
- ? relopts->vacuum_max_threshold
+ vac_max_thresh = (avopts && avopts->vacuum_max_threshold >= -1)
+ ? avopts->vacuum_max_threshold
: autovacuum_vac_max_thresh;
- vac_ins_scale_factor = (relopts && relopts->vacuum_ins_scale_factor >= 0)
- ? relopts->vacuum_ins_scale_factor
+ vac_ins_scale_factor = (avopts && avopts->vacuum_ins_scale_factor >= 0)
+ ? avopts->vacuum_ins_scale_factor
: autovacuum_vac_ins_scale;
/* -1 is used to disable insert vacuums */
- vac_ins_base_thresh = (relopts && relopts->vacuum_ins_threshold >= -1)
- ? relopts->vacuum_ins_threshold
+ vac_ins_base_thresh = (avopts && avopts->vacuum_ins_threshold >= -1)
+ ? avopts->vacuum_ins_threshold
: autovacuum_vac_ins_thresh;
- anl_scale_factor = (relopts && relopts->analyze_scale_factor >= 0)
- ? relopts->analyze_scale_factor
+ anl_scale_factor = (avopts && avopts->analyze_scale_factor >= 0)
+ ? avopts->analyze_scale_factor
: autovacuum_anl_scale;
- anl_base_thresh = (relopts && relopts->analyze_threshold >= 0)
- ? relopts->analyze_threshold
+ anl_base_thresh = (avopts && avopts->analyze_threshold >= 0)
+ ? avopts->analyze_threshold
: autovacuum_anl_thresh;
- freeze_max_age = (relopts && relopts->freeze_max_age >= 0)
- ? Min(relopts->freeze_max_age, autovacuum_freeze_max_age)
+ freeze_max_age = (avopts && avopts->freeze_max_age >= 0)
+ ? Min(avopts->freeze_max_age, autovacuum_freeze_max_age)
: autovacuum_freeze_max_age;
- multixact_freeze_max_age = (relopts && relopts->multixact_freeze_max_age >= 0)
- ? Min(relopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
+ multixact_freeze_max_age = (avopts && avopts->multixact_freeze_max_age >= 0)
+ ? Min(avopts->multixact_freeze_max_age, effective_multixact_freeze_max_age)
: effective_multixact_freeze_max_age;
- av_enabled = (relopts ? relopts->enabled : true);
+ av_enabled = (avopts ? avopts->enabled : true);
/* Force vacuum if table is at risk of wraparound */
xidForceLimit = recentXid - freeze_max_age;
--
2.39.5 (Apple Git-154)
v1-0002-autovac-resolve-relopts-before-vacuuming.patchtext/plain; charset=us-asciiDownload
From a3bbb1fdcf6a66719af84b763861e187cc5588bd Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Mon, 23 Jun 2025 14:42:39 -0500
Subject: [PATCH v1 2/4] autovac: resolve relopts before vacuuming
---
src/backend/commands/vacuum.c | 7 ++--
src/backend/postmaster/autovacuum.c | 52 +++++++++++++++++++++--------
2 files changed, 44 insertions(+), 15 deletions(-)
diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c
index 33a33bf6b1c..0015b9ef4b0 100644
--- a/src/backend/commands/vacuum.c
+++ b/src/backend/commands/vacuum.c
@@ -421,7 +421,7 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel)
/*
* Later, in vacuum_rel(), we check if a reloption override was specified.
*/
- params.max_eager_freeze_failure_rate = vacuum_max_eager_freeze_failure_rate;
+ params.max_eager_freeze_failure_rate = -1.0;
/*
* Create special memory context for cross-transaction storage.
@@ -2195,10 +2195,13 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params,
* Check if the vacuum_max_eager_freeze_failure_rate table storage
* parameter was specified. This overrides the GUC value.
*/
- if (rel->rd_options != NULL &&
+ if (params->max_eager_freeze_failure_rate < 0 &&
+ rel->rd_options != NULL &&
((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate >= 0)
params->max_eager_freeze_failure_rate =
((StdRdOptions *) rel->rd_options)->vacuum_max_eager_freeze_failure_rate;
+ else
+ params->max_eager_freeze_failure_rate = vacuum_max_eager_freeze_failure_rate;
/*
* Set truncate option based on truncate reloption or GUC if it wasn't
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index f86c9fed853..3bedca971ff 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -2758,6 +2758,9 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
int multixact_freeze_table_age;
int log_min_duration;
AutoVacOpts *avopts = (relopts ? &relopts->autovacuum : NULL);
+ VacOptValue index_cleanup;
+ VacOptValue truncate;
+ double max_eager_freeze_failure_rate;
/*
* Calculate the vacuum cost parameters and the freeze ages. If there
@@ -2790,6 +2793,39 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
? avopts->multixact_freeze_table_age
: default_multixact_freeze_table_age;
+ if (relopts)
+ {
+ switch (relopts->vacuum_index_cleanup)
+ {
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_AUTO:
+ index_cleanup = VACOPTVALUE_AUTO;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_ON:
+ index_cleanup = VACOPTVALUE_ENABLED;
+ break;
+ case STDRD_OPTION_VACUUM_INDEX_CLEANUP_OFF:
+ index_cleanup = VACOPTVALUE_DISABLED;
+ break;
+ }
+ }
+ else
+ index_cleanup = VACOPTVALUE_UNSPECIFIED;
+
+ if (relopts && relopts->vacuum_truncate_set)
+ {
+ if (relopts->vacuum_truncate)
+ truncate = VACOPTVALUE_ENABLED;
+ else
+ truncate = VACOPTVALUE_DISABLED;
+ }
+ else
+ truncate = VACOPTVALUE_UNSPECIFIED;
+
+ if (relopts && relopts->vacuum_max_eager_freeze_failure_rate >= 0)
+ max_eager_freeze_failure_rate = relopts->vacuum_max_eager_freeze_failure_rate;
+ else
+ max_eager_freeze_failure_rate = -1.0;
+
tab = palloc(sizeof(autovac_table));
tab->at_relid = relid;
tab->at_sharedrel = classForm->relisshared;
@@ -2806,13 +2842,8 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
(doanalyze ? VACOPT_ANALYZE : 0) |
(!wraparound ? VACOPT_SKIP_LOCKED : 0);
- /*
- * index_cleanup and truncate are unspecified at first in autovacuum.
- * They will be filled in with usable values using their reloptions
- * (or reloption defaults) later.
- */
- tab->at_params.index_cleanup = VACOPTVALUE_UNSPECIFIED;
- tab->at_params.truncate = VACOPTVALUE_UNSPECIFIED;
+ tab->at_params.index_cleanup = index_cleanup;
+ tab->at_params.truncate = truncate;
/* As of now, we don't support parallel vacuum for autovacuum */
tab->at_params.nworkers = -1;
tab->at_params.freeze_min_age = freeze_min_age;
@@ -2822,12 +2853,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map,
tab->at_params.is_wraparound = wraparound;
tab->at_params.log_min_duration = log_min_duration;
tab->at_params.toast_parent = InvalidOid;
-
- /*
- * Later, in vacuum_rel(), we check reloptions for any
- * vacuum_max_eager_freeze_failure_rate override.
- */
- tab->at_params.max_eager_freeze_failure_rate = vacuum_max_eager_freeze_failure_rate;
+ tab->at_params.max_eager_freeze_failure_rate = max_eager_freeze_failure_rate;
tab->at_storage_param_vac_cost_limit = avopts ?
avopts->vacuum_cost_limit : 0;
tab->at_storage_param_vac_cost_delay = avopts ?
--
2.39.5 (Apple Git-154)
On Mon, Jun 23, 2025 at 03:59:56PM -0500, Nathan Bossart wrote:
Here is a very rough proof-of-concept patch set for this. AFAICT there are
a few options we cannot fix on the back-branches because there is no way to
tell whether it is set or has just picked up the default. On v18 and
newer, we could use isset_offset, but that doesn't exist on older versions.
(I haven't looked closely, but I'm assuming that back-patching isset_offset
isn't an option.)
Hmm. I am wondering if we need to be aggressive about this set of
changes at all in the back branches. It's been broken for a long time
without anybody really complaining about the fact that reloptions
being set or not influenced the outcome in the context of autovacuum,
so perhaps there is a good argument for keeping all that in v19. My
conservative 2c.
I would like to explore the "option 2" from upthread [0] for v19. I think
that is a better long-term solution, and it may allow us to remove the
table_toast_map in autovacuum.
It would be nice to have some tests here to check the state of the
options used? My best guess would be a DEBUG1 entry combined with a
scan of the logs generated and an aggressive autovacuum worker
spawn to check that the options generated are what we expect for the
relations autovacuum picks up.
--
Michael
On Tue, Jun 24, 2025 at 02:10:55PM +0900, Michael Paquier wrote:
On Mon, Jun 23, 2025 at 03:59:56PM -0500, Nathan Bossart wrote:
Here is a very rough proof-of-concept patch set for this. AFAICT there are
a few options we cannot fix on the back-branches because there is no way to
tell whether it is set or has just picked up the default. On v18 and
newer, we could use isset_offset, but that doesn't exist on older versions.
(I haven't looked closely, but I'm assuming that back-patching isset_offset
isn't an option.)Hmm. I am wondering if we need to be aggressive about this set of
changes at all in the back branches. It's been broken for a long time
without anybody really complaining about the fact that reloptions
being set or not influenced the outcome in the context of autovacuum,
so perhaps there is a good argument for keeping all that in v19. My
conservative 2c.
Yeah, I'm tempted to even ask how folks feel about removing the toast.*
reloptions. Maybe there's some simple cases that work well enough, but
AFAICT any moderately-complicated setup basically doesn't work at all. In
any case, writing out this patch set has got me on the fix-on-HEAD-only
bandwagon.
I would like to explore the "option 2" from upthread [0] for v19. I think
that is a better long-term solution, and it may allow us to remove the
table_toast_map in autovacuum.It would be nice to have some tests here to check the state of the
options used? My best guess would be a DEBUG1 entry combined with a
scan of the logs generated and an aggressive autovacuum worker
spawn to check that the options generated are what we expect for the
relations autovacuum picks up.
Eh... I agree that's probably how we'd have to test it with the existing
tools, but it sure sounds like a recipe for a flaky test.
--
nathan
On Mon, Jun 23, 2025 at 10:59:51AM -0500, Nathan Bossart wrote:
On Sat, Jun 21, 2025 at 11:45:25PM -0400, shihao zhong wrote:
2) When updating a table's relopt, also update the relopt of its
associated TOAST table if it's not already set. Similarly, when
creating a new TOAST table, it would inherit the parent's relopt.Option 2 seems more reasonable to me, as it avoids requiring customers
to manually resolve these options, when they have different settings
for the parent and TOAST tables."I like this one, but since it won't fix existing clusters, it might only be
workable for v19.
Actually, I think there's a problem with this approach. If we set the
reloption for both the main relation and the TOAST table, then we won't
know what to do for RESET. Take the following examples:
ALTER TABLE test SET (vacuum_truncate = false);
ALTER TABLE test RESET (vacuum_truncate);
ALTER TABLE test SET (vacuum_truncate = false);
ALTER TABLE test SET (toast.vacuum_truncate = false);
ALTER TABLE test RESET (vacuum_truncate);
After executing the commands in the first stanza, you'd expect the
vacuum_truncate reloption to be unset for both the main relation and its
TOAST table. After the second one, you'd expect it to be set for only the
TOAST table. But unless there's some way to know the source of the TOAST
table's reloption, we can't know which behavior is correct at RESET time.
--
nathan
Actually, I think there's a problem with this approach...
You're right. I forgot we can reset the table options. While we could
use a placeholder and resolve it on-demand, that seems like too much
work.
On Wed, Jul 2, 2025 at 10:44 AM shihao zhong <zhong950419@gmail.com> wrote:
Actually, I think there's a problem with this approach...
You're right. I forgot we can reset the table options. While we could
use a placeholder and resolve it on-demand, that seems like too much
work.
Hi all,
I started a conversation about TOAST table vacuum truncation parameter
inheritance [1]/messages/by-id/CANqtF-pLHBDdNM-DifXQqVq+VWA3DTYOx4+3m2+TFJ1zDOZETg@mail.gmail.com and was pointed to this thread which I totally missed. I
recently came across the issue where `vacuum_truncate` on TOAST tables
wasn't being inherited even though the parent table had the setting, and
the documentation mentioned that it should work [2]https://www.postgresql.org/docs/current/sql-createtable.html#RELOPTION-VACUUM-TRUNCATE. I see that there are a
lot of good conversations here already.
I'm curious to hear what folks think about the approach where we implement
the inheritance logic directly in `vacuum_rel()` when `params.truncate ==
VACOPTVALUE_UNSPECIFIED`. This should address the point raised upthread
about `vacuum_rel()` not looking up the main relation's reloptions when
processing a TOAST and also what I discovered in [1]/messages/by-id/CANqtF-pLHBDdNM-DifXQqVq+VWA3DTYOx4+3m2+TFJ1zDOZETg@mail.gmail.com. For instance it
could be something like:
1. For TOAST tables: When no explicit `toast.vacuum_truncate` is set, scan
`pg_class.reltoastrelid` to find the parent table and inherit its
`vacuum_truncate` setting.
2. For manual VACUUM: Pass the final truncate decision from main table to
TOAST table in the `toast_vacuum_params`
3. For autovacuum: The inheritance happens naturally since autovacuum calls
`vacuum_rel()` for each relation independently
This basically teaches `vacuum_rel()` to consult the main relation's
reloptions for TOAST tables, which was the core issue identified upthread.
It handles both manual VACUUM and autovacuum scenarios in one place,
without changing function signatures (which helps with potential
backporting).
The execution-layer fix should also help us avoid the parameter
contamination issues that were fixed in commit 661643dedad9 (I believe?),
since we're doing the lookup fresh each time `vacuum_rel()` is called on a
TOAST table.
I realize backporting might not be preferred given this issue has existed
for a long time. However, that is precisely why I feel like it might be
worth patching it because, at least per the docs, my understanding was that
if `vacuum_truncate` is set on the parent table then it applies to the
TOAST as well and I wonder if there are others in the same boat as well.
Would something like this focused approach (just teaching `vacuum_rel()` to
look up parent reloptions for TOAST tables) could be a reasonable fix that
complements the broader reloptions work discussed upthread for v19 and also
backportable to v13 onwards?
I am happy to help with this and split out the changes as it makes sense as
well.
Thanks
Shayon
[1]: /messages/by-id/CANqtF-pLHBDdNM-DifXQqVq+VWA3DTYOx4+3m2+TFJ1zDOZETg@mail.gmail.com
/messages/by-id/CANqtF-pLHBDdNM-DifXQqVq+VWA3DTYOx4+3m2+TFJ1zDOZETg@mail.gmail.com
[2]: https://www.postgresql.org/docs/current/sql-createtable.html#RELOPTION-VACUUM-TRUNCATE
https://www.postgresql.org/docs/current/sql-createtable.html#RELOPTION-VACUUM-TRUNCATE