Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.94.2) (envelope-from ) id 1vE8SO-0061Bs-27 for pgsql-hackers@arkaria.postgresql.org; Wed, 29 Oct 2025 15:51:27 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.94.2) (envelope-from ) id 1vE8SL-0020VU-Ir for pgsql-hackers@arkaria.postgresql.org; Wed, 29 Oct 2025 15:51:24 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.94.2) (envelope-from ) id 1vE8SL-0020VM-00 for pgsql-hackers@lists.postgresql.org; Wed, 29 Oct 2025 15:51:24 +0000 Received: from mail-io1-xd2b.google.com ([2607:f8b0:4864:20::d2b]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.96) (envelope-from ) id 1vE8SH-004POg-2X for pgsql-hackers@postgresql.org; Wed, 29 Oct 2025 15:51:23 +0000 Received: by mail-io1-xd2b.google.com with SMTP id ca18e2360f4ac-93eab530884so1473839f.3 for ; Wed, 29 Oct 2025 08:51:21 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1761753081; x=1762357881; darn=postgresql.org; h=in-reply-to:content-transfer-encoding:content-disposition :mime-version:references:message-id:subject:cc:to:from:date:from:to :cc:subject:date:message-id:reply-to; bh=dYZDmbmQCvPAceQ/f8j5hhb2QoEw0EGojrQ8esW/xT4=; b=SaBDvYUNPZSGKNtQN6Xb/OvKgUPssuj00qpj6//coR4bPx4qaRObZDc0AfeWMNAtEo mu1KjiUJv/6uhC/79cK+tWFJp3jdqKOj9q10zk5KsNy/5TfXRMdwB2pcqqzMcj0QLxLL NFtQsyE05UziBebTbXXiGGE6rexE3kZpIQL+7Y7Utgj/CM45PEoItPxxFcU02s8BkibF HZVGR/Cb1nupnG3rMcP76Yb/M2FktTcXRx9YvXaIKglxIaUaEmUn6drrd906JHdzQ5Zx Rvcq73f2Eat/UwNcrDfUXmF4rcxg9Dc1hDGiWrJ0mZ/IJAfLUQv4GdybAJupDgtgo6lz 2NCw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1761753081; x=1762357881; h=in-reply-to:content-transfer-encoding:content-disposition :mime-version:references:message-id:subject:cc:to:from:date :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to; bh=dYZDmbmQCvPAceQ/f8j5hhb2QoEw0EGojrQ8esW/xT4=; b=OrQH52maIu9xrUdyYkAPUQ2NIe3GNWnplSbxNKb2HsIajmIxRtLKUxAnptrDqhWoj5 PLSEqNsZZgRVu8ptm9wPnlY0zurFRhURXLxi1CJJfPpx+v4JkV5OjT+cs5kIaxXqbEAN 7Km9QwMiQwiIL2Uhp5qG3mPYLA5b42eq+ctOnb+KjawVmlM/G6sFDiClwYoXZNEuwgE7 m6pTaRkVwY9iQMpvKeN5Pu9C+olDkujvIpSNhoyYDFJT/Nj3R+IGyt60jqmbEf83F0Aw NbSKJDFhkSlBa+xwoBuyO0rXwHD489uB9ymBUW5zC1B7xdUPR0VcHeFLoEIp6wh8Q4Do JoZg== X-Forwarded-Encrypted: i=1; AJvYcCWYAjcKxhx9qc0m/29Qbw8HA+5IGE64xF/TYXgRwaltpQoO04I+YdiCLVIKu4/Z6kuyUFSw6U8oQmnkG5XS@postgresql.org X-Gm-Message-State: AOJu0YxzUmNFIhPrLMQdywOuHtrdEJw9ws7NRnhj9F//WpMjsmasGmos TWL8tAXn7KTJmSKFBsXZ1jw7NIdLtkFOcr/CE5BJ15YHWZBTX0QOnD8t X-Gm-Gg: ASbGncuPXd16i5rI7L5aPv5CfvrcIw16sIPPX2ugSmlLXkXZxR4UrOgOF0xT58zihKP sWHMMWW4ZoJO4vAlJrVpJifo9saiQ5t3kvqm6XYkwRYvJQm/uIz47OmSK8EtZKnMqQB3IGYSOW+ STi0meES+oG9S/LOw2AasWj2kLahK9AySUJqiKF2GFSFNLT3Rq3j0CCef2BMakiLsP6eLcFFbgV 6BMEpGxwwd3XIHFT9IM5S1qMpWxzVerAH9fF0YIKt2ffCVk87Bi7FGkioNN8qVk3LusP6bNCj5w Dyhn0M1m6NH/k6x3z5HiqN7Yka4scsLhovqQvsMEsyW+1wNSIZaWjtvAbzDurhTN87MtnzW2o2Y hOkYCsMASKzpdKWdkZZok37hzr3P9b+09swV5wnX97pNFhjhdIZMLqGiyZ4lNi+ybQVAuc96142 9ucFuOBFz2G5ABnLmflr2LVY0XbX9G9TsNzyCA+If45clcgb3BdCF0dVHUN8orOwy42w== X-Google-Smtp-Source: AGHT+IFZriRv/JASco/a5g1CgelbHZYIptVmjcE0tvfpKOw2ux6bobEMoCPE5WyE1tWDE5oKTIMqGQ== X-Received: by 2002:a05:6602:3fc1:b0:93e:7c6c:c0b6 with SMTP id ca18e2360f4ac-945c982a463mr530918139f.15.1761753080836; Wed, 29 Oct 2025 08:51:20 -0700 (PDT) Received: from nathan (162-195-168-172.lightspeed.stlsmo.sbcglobal.net. [162.195.168.172]) by smtp.gmail.com with ESMTPSA id ca18e2360f4ac-94359effcefsm475208239f.11.2025.10.29.08.51.19 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 29 Oct 2025 08:51:20 -0700 (PDT) Date: Wed, 29 Oct 2025 10:51:18 -0500 From: Nathan Bossart To: Sami Imseih Cc: David Rowley , Robert Haas , Jeremy Schneider , pgsql-hackers@postgresql.org Subject: Re: another autovacuum scheduling thread Message-ID: References: MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="80LKapVVCE26vvPp" Content-Disposition: inline Content-Transfer-Encoding: 8bit In-Reply-To: List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --80LKapVVCE26vvPp Content-Type: text/plain; charset=utf-8 Content-Disposition: inline Content-Transfer-Encoding: 8bit On Tue, Oct 28, 2025 at 05:44:37PM -0500, Sami Imseih wrote: > My compiler is complaining about v6 > > "../src/backend/postmaster/autovacuum.c:3293:32: warning: operation on > ‘*score’ may be undefined [-Wsequence-point] > 3293 | *score = *score = Max(*score, (double) > instuples / Max(vacinsthresh, 1)); > [2/2] Linking target src/backend/postgres" > > shouldn't just be like below? > > *score =Max(*score, (double) instuples / Max(vacinsthresh, 1)); Oops. I fixed that typo in v7. -- nathan --80LKapVVCE26vvPp Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename=v7-0001-autovacuum-scheduling-improvements.patch From a225d5965286c87403bdcad2aaad0ec5def7e115 Mon Sep 17 00:00:00 2001 From: Nathan Bossart Date: Fri, 10 Oct 2025 12:28:37 -0500 Subject: [PATCH v7 1/1] autovacuum scheduling improvements --- src/backend/postmaster/autovacuum.c | 197 ++++++++++++++++++++++------ src/tools/pgindent/typedefs.list | 1 + 2 files changed, 158 insertions(+), 40 deletions(-) diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 5084af7dfb6..e48bb06253b 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -97,6 +97,7 @@ #include "storage/procsignal.h" #include "storage/smgr.h" #include "tcop/tcopprot.h" +#include "utils/float.h" #include "utils/fmgroids.h" #include "utils/fmgrprotos.h" #include "utils/guc_hooks.h" @@ -310,6 +311,12 @@ static AutoVacuumShmemStruct *AutoVacuumShmem; static dlist_head DatabaseList = DLIST_STATIC_INIT(DatabaseList); static MemoryContext DatabaseListCxt = NULL; +typedef struct +{ + Oid oid; + double score; +} TableToProcess; + /* * Dummy pointer to persuade Valgrind that we've not leaked the array of * avl_dbase structs. Make it global to ensure the compiler doesn't @@ -351,7 +358,8 @@ static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts, Form_pg_class classForm, PgStat_StatTabEntry *tabentry, int effective_multixact_freeze_max_age, - bool *dovacuum, bool *doanalyze, bool *wraparound); + bool *dovacuum, bool *doanalyze, bool *wraparound, + double *score); static void autovacuum_do_vac_analyze(autovac_table *tab, BufferAccessStrategy bstrategy); @@ -1889,6 +1897,15 @@ get_database_list(void) return dblist; } +static int +TableToProcessComparator(const ListCell *a, const ListCell *b) +{ + TableToProcess *t1 = (TableToProcess *) lfirst(a); + TableToProcess *t2 = (TableToProcess *) lfirst(b); + + return float8_cmp_internal(t2->score, t1->score); +} + /* * Process a database table-by-table * @@ -1902,7 +1919,7 @@ do_autovacuum(void) HeapTuple tuple; TableScanDesc relScan; Form_pg_database dbForm; - List *table_oids = NIL; + List *tables_to_process = NIL; List *orphan_oids = NIL; HASHCTL ctl; HTAB *table_toast_map; @@ -2014,6 +2031,7 @@ do_autovacuum(void) bool dovacuum; bool doanalyze; bool wraparound; + double score = 0.0; if (classForm->relkind != RELKIND_RELATION && classForm->relkind != RELKIND_MATVIEW) @@ -2054,11 +2072,19 @@ do_autovacuum(void) /* Check if it needs vacuum or analyze */ relation_needs_vacanalyze(relid, relopts, classForm, tabentry, effective_multixact_freeze_max_age, - &dovacuum, &doanalyze, &wraparound); + &dovacuum, &doanalyze, &wraparound, + &score); - /* Relations that need work are added to table_oids */ + /* Relations that need work are added to tables_to_process */ if (dovacuum || doanalyze) - table_oids = lappend_oid(table_oids, relid); + { + TableToProcess *table = palloc(sizeof(TableToProcess)); + + table->oid = relid; + table->score = score; + + tables_to_process = lappend(tables_to_process, table); + } /* * Remember TOAST associations for the second pass. Note: we must do @@ -2114,6 +2140,7 @@ do_autovacuum(void) bool dovacuum; bool doanalyze; bool wraparound; + double score = 0.0; /* * We cannot safely process other backends' temp tables, so skip 'em. @@ -2146,11 +2173,19 @@ do_autovacuum(void) relation_needs_vacanalyze(relid, relopts, classForm, tabentry, effective_multixact_freeze_max_age, - &dovacuum, &doanalyze, &wraparound); + &dovacuum, &doanalyze, &wraparound, + &score); /* ignore analyze for toast tables */ if (dovacuum) - table_oids = lappend_oid(table_oids, relid); + { + TableToProcess *table = palloc(sizeof(TableToProcess)); + + table->oid = relid; + table->score = score; + + tables_to_process = lappend(tables_to_process, table); + } /* Release stuff to avoid leakage */ if (free_relopts) @@ -2274,6 +2309,8 @@ do_autovacuum(void) MemoryContextSwitchTo(AutovacMemCxt); } + list_sort(tables_to_process, TableToProcessComparator); + /* * Optionally, create a buffer access strategy object for VACUUM to use. * We use the same BufferAccessStrategy object for all tables VACUUMed by @@ -2302,9 +2339,9 @@ do_autovacuum(void) /* * Perform operations on collected tables. */ - foreach(cell, table_oids) + foreach_ptr(TableToProcess, table, tables_to_process) { - Oid relid = lfirst_oid(cell); + Oid relid = table->oid; HeapTuple classTup; autovac_table *tab; bool isshared; @@ -2535,7 +2572,7 @@ deleted: pg_atomic_test_set_flag(&MyWorkerInfo->wi_dobalance); } - list_free(table_oids); + list_free_deep(tables_to_process); /* * Perform additional work items, as requested by backends. @@ -2934,6 +2971,7 @@ recheck_relation_needs_vacanalyze(Oid relid, bool *wraparound) { PgStat_StatTabEntry *tabentry; + double score; /* fetch the pgstat table entry */ tabentry = pgstat_fetch_stat_tabentry_ext(classForm->relisshared, @@ -2941,15 +2979,12 @@ recheck_relation_needs_vacanalyze(Oid relid, relation_needs_vacanalyze(relid, avopts, classForm, tabentry, effective_multixact_freeze_max_age, - dovacuum, doanalyze, wraparound); + dovacuum, doanalyze, wraparound, + &score); /* Release tabentry to avoid leakage */ if (tabentry) pfree(tabentry); - - /* ignore ANALYZE for toast tables */ - if (classForm->relkind == RELKIND_TOASTVALUE) - *doanalyze = false; } /* @@ -2990,6 +3025,32 @@ recheck_relation_needs_vacanalyze(Oid relid, * autovacuum_vacuum_threshold GUC variable. Similarly, a vac_scale_factor * value < 0 is substituted with the value of * autovacuum_vacuum_scale_factor GUC variable. Ditto for analyze. + * + * This function also returns a score that can be used to sort the list of + * tables to process. The idea is to have autovacuum prioritize tables that + * are furthest beyond their thresholds (e.g., a table nearing transaction ID + * wraparound should be vacuumed first). This prioritization scheme is + * certainly far from perfect; there are simply too many possibilities for any + * scoring technique to work across all workloads, and the situation might + * change significantly between the time we calculate the score and the time + * that autovacuum gets to processing it. However, we have attempted to + * develop something that is expected to work for a large portion of workloads + * with reasonable parameter settings. + * + * The score is calculated as the maximum of the ratios of each of the table's + * relevant values to its threshold. For example, if the number of inserted + * tuples is 100, and the insert threshold for the table is 80, the insert + * score is 1.25. If all other scores are below that value, the returned score + * will be 1.25. The other criteria considered for the score are the table + * ages (both relfrozenxid and relminmxid) compared to the corresponding + * freeze-max-age setting, the number of updated/deleted tuples compared to the + * vacuum threshold, and the number of inserted/updated/deleted tuples compared + * to the analyze threshold. + * + * One exception to the previous paragraph is for tables nearing wraparound, + * i.e., those that have surpassed the effective failsafe ages. In that case, + * the relfrozen/relminmxid-based score is scaled aggressively so that the + * table has a decent chance of sorting to the top of the list. */ static void relation_needs_vacanalyze(Oid relid, @@ -3000,7 +3061,8 @@ relation_needs_vacanalyze(Oid relid, /* output params below */ bool *dovacuum, bool *doanalyze, - bool *wraparound) + bool *wraparound, + double *score) { bool force_vacuum; bool av_enabled; @@ -3029,11 +3091,16 @@ relation_needs_vacanalyze(Oid relid, int multixact_freeze_max_age; TransactionId xidForceLimit; TransactionId relfrozenxid; + TransactionId relminmxid; MultiXactId multiForceLimit; Assert(classForm != NULL); Assert(OidIsValid(relid)); + /* initialize variables that aren't guaranteed to be set below */ + *score = 0.0; + *doanalyze = false; + /* * Determine vacuum/analyze equation parameters. We have two possible * sources: the passed reloptions (which could be a main table or a toast @@ -3081,32 +3148,73 @@ relation_needs_vacanalyze(Oid relid, av_enabled = (relopts ? relopts->enabled : true); + relfrozenxid = classForm->relfrozenxid; + relminmxid = classForm->relminmxid; + /* Force vacuum if table is at risk of wraparound */ xidForceLimit = recentXid - freeze_max_age; if (xidForceLimit < FirstNormalTransactionId) xidForceLimit -= FirstNormalTransactionId; - relfrozenxid = classForm->relfrozenxid; force_vacuum = (TransactionIdIsNormal(relfrozenxid) && TransactionIdPrecedes(relfrozenxid, xidForceLimit)); if (!force_vacuum) { - MultiXactId relminmxid = classForm->relminmxid; - multiForceLimit = recentMulti - multixact_freeze_max_age; if (multiForceLimit < FirstMultiXactId) multiForceLimit -= FirstMultiXactId; force_vacuum = MultiXactIdIsValid(relminmxid) && MultiXactIdPrecedes(relminmxid, multiForceLimit); } - *wraparound = force_vacuum; + *wraparound = *dovacuum = force_vacuum; + + /* Update the score. */ + if (force_vacuum) + { + Oid xid_age; + Oid mxid_age; + double xid_score; + double mxid_score; + 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; + + xid_score = (double) xid_age / freeze_max_age; + mxid_score = (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. + */ + 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 (xid_age >= effective_xid_failsafe_age) + xid_score = pow(xid_score, Max(1.0, (double) xid_age / 100000000)); + if (mxid_age >= effective_mxid_failsafe_age) + mxid_score = pow(mxid_score, Max(1.0, (double) mxid_age / 100000000)); + + *score = Max(xid_score, mxid_score); + } /* User disabled it in pg_class.reloptions? (But ignore if at risk) */ if (!av_enabled && !force_vacuum) - { - *doanalyze = false; - *dovacuum = false; return; - } /* * If we found stats for the table, and autovacuum is currently enabled, @@ -3169,25 +3277,34 @@ relation_needs_vacanalyze(Oid relid, NameStr(classForm->relname), vactuples, vacthresh, anltuples, anlthresh); - /* Determine if this table needs vacuum or analyze. */ - *dovacuum = force_vacuum || (vactuples > vacthresh) || - (vac_ins_base_thresh >= 0 && instuples > vacinsthresh); - *doanalyze = (anltuples > anlthresh); - } - else - { /* - * Skip a table not found in stat hash, unless we have to force vacuum - * for anti-wrap purposes. If it's not acted upon, there's no need to - * vacuum it. + * Determine if this table needs vacuum, and update the score if it + * does. */ - *dovacuum = force_vacuum; - *doanalyze = false; - } + if (vactuples > vacthresh) + { + *dovacuum = true; + *score = Max(*score, (double) vactuples / Max(vacthresh, 1)); + } + + if (vac_ins_base_thresh >= 0 && instuples > vacinsthresh) + { + *dovacuum = true; + *score = Max(*score, (double) instuples / Max(vacinsthresh, 1)); + } - /* ANALYZE refuses to work with pg_statistic */ - if (relid == StatisticRelationId) - *doanalyze = false; + /* + * Determine if this table needs analyze, and update the score if it + * does. Note that we don't analyze TOAST tables and pg_statistic. + */ + if (anltuples > anlthresh && + relid != StatisticRelationId && + classForm->relkind != RELKIND_TOASTVALUE) + { + *doanalyze = true; + *score = Max(*score, (double) anltuples / Max(anlthresh, 1)); + } + } } /* diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index ac2da4c98cf..bb977229f75 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3007,6 +3007,7 @@ TableScanDesc TableScanDescData TableSpaceCacheEntry TableSpaceOpts +TableToProcess TablespaceList TablespaceListCell TapeBlockTrailer -- 2.39.5 (Apple Git-154) --80LKapVVCE26vvPp--