From 53b8ca6881a1578fc47db47a2754faba754a2b7c Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Fri, 3 Apr 2026 16:44:41 -0500 Subject: [PATCH] Teach relation_needs_vacanalyze() to always compute scores. Presently, this function only computes component scores when the corresponding threshold is reached. A follow-up commit will add a view that shows tables' autovacuum scores, and we anticipate that users will want to use this view to discover tables that are nearing autovacuum eligibility. This commit teaches this function to always compute autovacuum scores, even when a threshold has not been reached or autovacuum is disabled. The restructuring in this commit revealed an interesting edge case. If the table needs vacuuming for wraparound prevention and autovacuum is disabled for it, we might still choose to analyze it. It's not clear if this is intentional, but it has been this way for nearly 20 years, so it seems best to avoid changing it without further discussion. Author: Sami Imseih Reviewed-by: Bharath Rupireddy Discussion: https://postgr.es/m/CAA5RZ0s4xjMrB-VAnLccC7kY8d0-4806-Lsac-czJsdA1LXtAw%40mail.gmail.com --- src/backend/postmaster/autovacuum.c | 146 ++++++++++++++-------------- 1 file changed, 72 insertions(+), 74 deletions(-) diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 590e4c8e44c..93dee70c1de 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -3122,6 +3122,10 @@ relation_needs_vacanalyze(Oid relid, TransactionId relfrozenxid; MultiXactId relminmxid; MultiXactId multiForceLimit; + uint32 xid_age; + uint32 mxid_age; + int effective_xid_failsafe_age; + int effective_mxid_failsafe_age; float4 pcnt_unfrozen = 1; float4 reltuples = classForm->reltuples; @@ -3181,6 +3185,7 @@ relation_needs_vacanalyze(Oid relid, : effective_multixact_freeze_max_age; av_enabled = (relopts ? relopts->enabled : true); + av_enabled &= AutoVacuumingActive(); relfrozenxid = classForm->relfrozenxid; relminmxid = classForm->relminmxid; @@ -3201,65 +3206,52 @@ relation_needs_vacanalyze(Oid relid, } *wraparound = force_vacuum; - /* Update the score. */ + /* + * To calculate the (M)XID age portion of the score, divide the age by its + * respective *_freeze_max_age parameter. + */ + xid_age = TransactionIdIsNormal(relfrozenxid) ? recentXid - relfrozenxid : 0; + mxid_age = MultiXactIdIsValid(relminmxid) ? recentMulti - relminmxid : 0; + + scores->xid = (double) xid_age / freeze_max_age; + scores->mxid = (double) mxid_age / multixact_freeze_max_age; + + /* + * To ensure tables are given increased priority once they begin + * approaching wraparound, we scale the score aggressively if the ages + * surpass vacuum_failsafe_age or vacuum_multixact_failsafe_age. + * + * As in vacuum_xid_failsafe_check(), the effective failsafe age is no + * less than 105% the value of the respective *_freeze_max_age parameter. + * Note that per-table settings could result in a low score even if the + * table surpasses the failsafe settings. However, this is a strange + * enough corner case that we don't bother trying to handle it. + * + * We further adjust the effective failsafe ages with the weight + * parameters so that increasing them lowers the ages at which we begin + * scaling aggressively. + */ + effective_xid_failsafe_age = Max(vacuum_failsafe_age, + autovacuum_freeze_max_age * 1.05); + effective_mxid_failsafe_age = Max(vacuum_multixact_failsafe_age, + autovacuum_multixact_freeze_max_age * 1.05); + + if (autovacuum_freeze_score_weight > 1.0) + effective_xid_failsafe_age /= autovacuum_freeze_score_weight; + if (autovacuum_multixact_freeze_score_weight > 1.0) + effective_mxid_failsafe_age /= autovacuum_multixact_freeze_score_weight; + + if (xid_age >= effective_xid_failsafe_age) + scores->xid = pow(scores->xid, Max(1.0, (double) xid_age / 100000000)); + if (mxid_age >= effective_mxid_failsafe_age) + scores->mxid = pow(scores->mxid, Max(1.0, (double) mxid_age / 100000000)); + + scores->xid *= autovacuum_freeze_score_weight; + scores->mxid *= autovacuum_multixact_freeze_score_weight; + + scores->max = Max(scores->xid, scores->mxid); if (force_vacuum) - { - uint32 xid_age; - uint32 mxid_age; - int effective_xid_failsafe_age; - int effective_mxid_failsafe_age; - - /* - * To calculate the (M)XID age portion of the score, divide the age by - * its respective *_freeze_max_age parameter. - */ - xid_age = TransactionIdIsNormal(relfrozenxid) ? recentXid - relfrozenxid : 0; - mxid_age = MultiXactIdIsValid(relminmxid) ? recentMulti - relminmxid : 0; - - scores->xid = (double) xid_age / freeze_max_age; - scores->mxid = (double) mxid_age / multixact_freeze_max_age; - - /* - * To ensure tables are given increased priority once they begin - * approaching wraparound, we scale the score aggressively if the ages - * surpass vacuum_failsafe_age or vacuum_multixact_failsafe_age. - * - * As in vacuum_xid_failsafe_check(), the effective failsafe age is no - * less than 105% the value of the respective *_freeze_max_age - * parameter. Note that per-table settings could result in a low - * score even if the table surpasses the failsafe settings. However, - * this is a strange enough corner case that we don't bother trying to - * handle it. - * - * We further adjust the effective failsafe ages with the weight - * parameters so that increasing them lowers the ages at which we - * begin scaling aggressively. - */ - effective_xid_failsafe_age = Max(vacuum_failsafe_age, - autovacuum_freeze_max_age * 1.05); - effective_mxid_failsafe_age = Max(vacuum_multixact_failsafe_age, - autovacuum_multixact_freeze_max_age * 1.05); - - if (autovacuum_freeze_score_weight > 1.0) - effective_xid_failsafe_age /= autovacuum_freeze_score_weight; - if (autovacuum_multixact_freeze_score_weight > 1.0) - effective_mxid_failsafe_age /= autovacuum_multixact_freeze_score_weight; - - if (xid_age >= effective_xid_failsafe_age) - scores->xid = pow(scores->xid, Max(1.0, (double) xid_age / 100000000)); - if (mxid_age >= effective_mxid_failsafe_age) - scores->mxid = pow(scores->mxid, Max(1.0, (double) mxid_age / 100000000)); - - scores->xid *= autovacuum_freeze_score_weight; - scores->mxid *= autovacuum_multixact_freeze_score_weight; - - scores->max = Max(scores->xid, scores->mxid); *dovacuum = true; - } - - /* User disabled it in pg_class.reloptions? (But ignore if at risk) */ - if (!av_enabled && !force_vacuum) - return; /* * If we found stats for the table, and autovacuum is currently enabled, @@ -3268,7 +3260,7 @@ relation_needs_vacanalyze(Oid relid, * vacuuming only, so don't vacuum (or analyze) anything that's not being * forced. */ - if (!tabentry || !AutoVacuumingActive()) + if (!tabentry) return; vactuples = tabentry->dead_tuples; @@ -3304,37 +3296,43 @@ relation_needs_vacanalyze(Oid relid, vac_ins_scale_factor * reltuples * pcnt_unfrozen; anlthresh = (float4) anl_base_thresh + anl_scale_factor * reltuples; - /* - * Determine if this table needs vacuum, and update the score if it does. - */ - if (vactuples > vacthresh) - { - scores->vac = (double) vactuples / Max(vacthresh, 1); - scores->vac *= autovacuum_vacuum_score_weight; - scores->max = Max(scores->max, scores->vac); + /* Determine if this table needs vacuum, and update the score. */ + scores->vac = (double) vactuples / Max(vacthresh, 1); + scores->vac *= autovacuum_vacuum_score_weight; + scores->max = Max(scores->max, scores->vac); + if (av_enabled && vactuples > vacthresh) *dovacuum = true; - } - if (vac_ins_base_thresh >= 0 && instuples > vacinsthresh) + if (vac_ins_base_thresh >= 0) { scores->vac_ins = (double) instuples / Max(vacinsthresh, 1); scores->vac_ins *= autovacuum_vacuum_insert_score_weight; scores->max = Max(scores->max, scores->vac_ins); - *dovacuum = true; + if (av_enabled && instuples > vacinsthresh) + *dovacuum = true; } /* - * Determine if this table needs analyze, and update the score if it does. - * Note that we don't analyze TOAST tables and pg_statistic. + * Determine if this table needs analyze, and update the score. Note that + * we don't analyze TOAST tables and pg_statistic. */ - if (anltuples > anlthresh && - relid != StatisticRelationId && + if (relid != StatisticRelationId && classForm->relkind != RELKIND_TOASTVALUE) { scores->anl = (double) anltuples / Max(anlthresh, 1); scores->anl *= autovacuum_analyze_score_weight; scores->max = Max(scores->max, scores->anl); - *doanalyze = true; + if (av_enabled && anltuples > anlthresh) + *doanalyze = true; + + /* + * For historical reasons, we analyze even when autovacuum is disabled + * for the table if at risk of wraparound. It's not clear if this is + * intentional, but it has been this way for a very long time, so it + * seems best to avoid changing it without further discussion. + */ + if (force_vacuum && AutoVacuumingActive() && anltuples > anlthresh) + *doanalyze = true; } if (vac_ins_base_thresh >= 0)