agora inbox for pgsql-hackers@postgresql.org
help / color / mirror / Atom feedFrom: Nathan Bossart <nathandbossart@gmail.com>
To: Masahiko Sawada <sawada.mshk@gmail.com>
Cc: PostgreSQL-development <pgsql-hackers@postgresql.org>
Subject: Re: pg_stat_get_autovacuum_scores ignores the main table's reloptions for TOAST tables
Date: Fri, 28 Aug 2026 10:16:09 -0500
Message-ID: <apGmOZeIlyW-Y1f6@nathan> (raw)
In-Reply-To: <apGjEvo15Z5oKHEM@nathan>
References: <CAD21AoB1CJRVfCDh8qYuD3eueiygXxk7F3nybgjN0RZXSD-QUw@mail.gmail.com>
<apGJqfd2xyAcUGKZ@nathan>
<apGjEvo15Z5oKHEM@nathan>
On Fri, Aug 28, 2026 at 10:02:42AM -0500, Nathan Bossart wrote:
> Here is a patch.
Sorry for the noise. I noticed some silly mistakes in v1, so here's a v2
with those fixed.
--
nathan
From 704573f8367e8065f9e7d02d3af02f21a4870ace Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Aug 2026 09:54:06 -0500
Subject: [PATCH v2 1/1] Fix pg_stat_autovacuum_scores for TOAST tables.
In v19, pg_stat_autovacuum_scores computes a TOAST table's scores
from its own storage parameters alone. Autovacuum instead falls
back to the main table's parameters when the TOAST table has none
of its own, so the view may report scores that don't match what
autovacuum would calculate. This contradicts the documented
promise that the view generates its results the same way autovacuum
workers do. To fix, teach the view to do the same fallback. As in
do_autovacuum(), we cannot know a TOAST table's parameters until we
have seen its main relation, so the view now makes a preliminary
pass over pg_class to collect the main relations' parameters.
Commit fad70a09ff for v20 improved autovacuum's handling of TOAST
storage parameters and adjusted the view to match, but it was
deemed too intrusive to back-patch. This fix is for v19 only.
Oversight in commit 87f61f0c82.
Reported-by: Masahiko Sawada <sawada.mshk@gmail.com>
Discussion: https://postgr.es/m/CAD21AoB1CJRVfCDh8qYuD3eueiygXxk7F3nybgjN0RZXSD-QUw%40mail.gmail.com
Backpatch-through: 19 only
---
src/backend/postmaster/autovacuum.c | 75 ++++++++++++++++++++++++++++-
1 file changed, 73 insertions(+), 2 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 0c975d0eda6..badd545ba0e 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -3652,6 +3652,8 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
TableScanDesc scan;
HeapTuple tup;
ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HTAB *table_toast_map;
+ HASHCTL ctl;
InitMaterializedSRF(fcinfo, 0);
@@ -3660,13 +3662,64 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
recentXid = ReadNextTransactionId();
recentMulti = ReadNextMultiXactId();
- /* scan pg_class */
+ /* create hash table for toast <-> main relid mapping */
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(av_relation);
+ ctl.hcxt = CurrentMemoryContext;
+ table_toast_map = hash_create("TOAST to main relid map",
+ 100,
+ &ctl,
+ HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+
rel = table_open(RelationRelationId, AccessShareLock);
+
+ /*
+ * Do an initial pass over pg_class to collect the main relations'
+ * autovacuum parameters, which a TOAST table with none of its own
+ * inherits. We cannot gather these as we go, since a TOAST table may
+ * precede its main relation in the scan.
+ */
+ scan = table_beginscan_catalog(rel, 0, NULL);
+ while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
+ AutoVacOpts *avopts;
+ av_relation *hentry;
+ bool found;
+
+ /* skip ineligible entries */
+ if (form->relkind != RELKIND_RELATION &&
+ form->relkind != RELKIND_MATVIEW)
+ continue;
+ if (form->relpersistence == RELPERSISTENCE_TEMP)
+ continue;
+ if (!OidIsValid(form->reltoastrelid))
+ continue;
+
+ avopts = extract_autovac_opts(tup, RelationGetDescr(rel));
+ if (avopts == NULL)
+ continue;
+
+ hentry = hash_search(table_toast_map, &form->reltoastrelid,
+ HASH_ENTER, &found);
+ Assert(!found); /* rels cannot share a TOAST table */
+
+ /* hash_search already filled in the key */
+ hentry->ar_relid = form->oid;
+ hentry->ar_hasrelopts = true;
+ memcpy(&hentry->ar_reloptions, avopts, sizeof(AutoVacOpts));
+
+ pfree(avopts);
+ }
+ table_endscan(scan);
+
+ /* now scan pg_class again to compute the scores */
scan = table_beginscan_catalog(rel, 0, NULL);
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
AutoVacOpts *avopts;
+ bool free_avopts = false;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3682,13 +3735,30 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
if (form->relpersistence == RELPERSISTENCE_TEMP)
continue;
+ /*
+ * fetch reloptions -- if this toast table does not have them, try the
+ * main rel
+ */
avopts = extract_autovac_opts(tup, RelationGetDescr(rel));
+ if (avopts)
+ free_avopts = true;
+ else if (form->relkind == RELKIND_TOASTVALUE)
+ {
+ av_relation *hentry;
+ bool found;
+
+ hentry = hash_search(table_toast_map, &form->oid,
+ HASH_FIND, &found);
+ if (found && hentry->ar_hasrelopts)
+ avopts = &hentry->ar_reloptions;
+ }
+
relation_needs_vacanalyze(form->oid, avopts, form,
effective_multixact_freeze_max_age,
LOG_NEVER,
&dovacuum, &doanalyze, &wraparound,
&scores);
- if (avopts)
+ if (free_avopts)
pfree(avopts);
vals[0] = ObjectIdGetDatum(form->oid);
@@ -3706,6 +3776,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
}
table_endscan(scan);
table_close(rel, AccessShareLock);
+ hash_destroy(table_toast_map);
return (Datum) 0;
}
--
2.55.0
Attachments:
[text/plain] v2-0001-Fix-pg_stat_autovacuum_scores-for-TOAST-tables.patch (5.0K, ../apGmOZeIlyW-Y1f6@nathan/2-v2-0001-Fix-pg_stat_autovacuum_scores-for-TOAST-tables.patch)
download | inline diff:
From 704573f8367e8065f9e7d02d3af02f21a4870ace Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathan@postgresql.org>
Date: Fri, 28 Aug 2026 09:54:06 -0500
Subject: [PATCH v2 1/1] Fix pg_stat_autovacuum_scores for TOAST tables.
In v19, pg_stat_autovacuum_scores computes a TOAST table's scores
from its own storage parameters alone. Autovacuum instead falls
back to the main table's parameters when the TOAST table has none
of its own, so the view may report scores that don't match what
autovacuum would calculate. This contradicts the documented
promise that the view generates its results the same way autovacuum
workers do. To fix, teach the view to do the same fallback. As in
do_autovacuum(), we cannot know a TOAST table's parameters until we
have seen its main relation, so the view now makes a preliminary
pass over pg_class to collect the main relations' parameters.
Commit fad70a09ff for v20 improved autovacuum's handling of TOAST
storage parameters and adjusted the view to match, but it was
deemed too intrusive to back-patch. This fix is for v19 only.
Oversight in commit 87f61f0c82.
Reported-by: Masahiko Sawada <sawada.mshk@gmail.com>
Discussion: https://postgr.es/m/CAD21AoB1CJRVfCDh8qYuD3eueiygXxk7F3nybgjN0RZXSD-QUw%40mail.gmail.com
Backpatch-through: 19 only
---
src/backend/postmaster/autovacuum.c | 75 ++++++++++++++++++++++++++++-
1 file changed, 73 insertions(+), 2 deletions(-)
diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
index 0c975d0eda6..badd545ba0e 100644
--- a/src/backend/postmaster/autovacuum.c
+++ b/src/backend/postmaster/autovacuum.c
@@ -3652,6 +3652,8 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
TableScanDesc scan;
HeapTuple tup;
ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+ HTAB *table_toast_map;
+ HASHCTL ctl;
InitMaterializedSRF(fcinfo, 0);
@@ -3660,13 +3662,64 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
recentXid = ReadNextTransactionId();
recentMulti = ReadNextMultiXactId();
- /* scan pg_class */
+ /* create hash table for toast <-> main relid mapping */
+ ctl.keysize = sizeof(Oid);
+ ctl.entrysize = sizeof(av_relation);
+ ctl.hcxt = CurrentMemoryContext;
+ table_toast_map = hash_create("TOAST to main relid map",
+ 100,
+ &ctl,
+ HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
+
rel = table_open(RelationRelationId, AccessShareLock);
+
+ /*
+ * Do an initial pass over pg_class to collect the main relations'
+ * autovacuum parameters, which a TOAST table with none of its own
+ * inherits. We cannot gather these as we go, since a TOAST table may
+ * precede its main relation in the scan.
+ */
+ scan = table_beginscan_catalog(rel, 0, NULL);
+ while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
+ {
+ Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
+ AutoVacOpts *avopts;
+ av_relation *hentry;
+ bool found;
+
+ /* skip ineligible entries */
+ if (form->relkind != RELKIND_RELATION &&
+ form->relkind != RELKIND_MATVIEW)
+ continue;
+ if (form->relpersistence == RELPERSISTENCE_TEMP)
+ continue;
+ if (!OidIsValid(form->reltoastrelid))
+ continue;
+
+ avopts = extract_autovac_opts(tup, RelationGetDescr(rel));
+ if (avopts == NULL)
+ continue;
+
+ hentry = hash_search(table_toast_map, &form->reltoastrelid,
+ HASH_ENTER, &found);
+ Assert(!found); /* rels cannot share a TOAST table */
+
+ /* hash_search already filled in the key */
+ hentry->ar_relid = form->oid;
+ hentry->ar_hasrelopts = true;
+ memcpy(&hentry->ar_reloptions, avopts, sizeof(AutoVacOpts));
+
+ pfree(avopts);
+ }
+ table_endscan(scan);
+
+ /* now scan pg_class again to compute the scores */
scan = table_beginscan_catalog(rel, 0, NULL);
while ((tup = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_class form = (Form_pg_class) GETSTRUCT(tup);
AutoVacOpts *avopts;
+ bool free_avopts = false;
bool dovacuum;
bool doanalyze;
bool wraparound;
@@ -3682,13 +3735,30 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
if (form->relpersistence == RELPERSISTENCE_TEMP)
continue;
+ /*
+ * fetch reloptions -- if this toast table does not have them, try the
+ * main rel
+ */
avopts = extract_autovac_opts(tup, RelationGetDescr(rel));
+ if (avopts)
+ free_avopts = true;
+ else if (form->relkind == RELKIND_TOASTVALUE)
+ {
+ av_relation *hentry;
+ bool found;
+
+ hentry = hash_search(table_toast_map, &form->oid,
+ HASH_FIND, &found);
+ if (found && hentry->ar_hasrelopts)
+ avopts = &hentry->ar_reloptions;
+ }
+
relation_needs_vacanalyze(form->oid, avopts, form,
effective_multixact_freeze_max_age,
LOG_NEVER,
&dovacuum, &doanalyze, &wraparound,
&scores);
- if (avopts)
+ if (free_avopts)
pfree(avopts);
vals[0] = ObjectIdGetDatum(form->oid);
@@ -3706,6 +3776,7 @@ pg_stat_get_autovacuum_scores(PG_FUNCTION_ARGS)
}
table_endscan(scan);
table_close(rel, AccessShareLock);
+ hash_destroy(table_toast_map);
return (Datum) 0;
}
--
2.55.0
view thread (8+ messages) latest in thread
Message-ID: <apGmOZeIlyW-Y1f6@nathan>
Permalink: ../apGmOZeIlyW-Y1f6@nathan/
Also on: postgresql.org/message-id/apGmOZeIlyW-Y1f6@nathan
reply
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: pgsql-hackers@postgresql.org
Cc: nathandbossart@gmail.com, sawada.mshk@gmail.com
Subject: Re: pg_stat_get_autovacuum_scores ignores the main table's reloptions for TOAST tables
In-Reply-To: <apGmOZeIlyW-Y1f6@nathan>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox