From 89fd5d01e34d8f27a0ac20af21935d98fbe710df Mon Sep 17 00:00:00 2001 From: Jan Nidzwetzki Date: Mon, 20 Jul 2026 11:45:33 +0200 Subject: [PATCH] Recover from non-finite reltuples via autoanalyze If pg_class.reltuples is infinity or NaN, the analyze threshold computed in relation_needs_vacanalyze() (anl_base_thresh + anl_scale_factor * reltuples) is non-finite too, so "anltuples > anlthresh" is never true and autoanalyze never runs. Since ANALYZE is what recomputes reltuples, the bogus value can then never be corrected on its own. Force an analyze whenever reltuples is not finite, so the statistics are recomputed from a fresh sample and a sane value is restored. Add a TAP test to the test_autovacuum module that plants an infinity into pg_class.reltuples and checks that autoanalyze recomputes a finite value. --- src/backend/postmaster/autovacuum.c | 10 +++- src/test/modules/test_autovacuum/meson.build | 1 + .../t/002_reltuples_nonfinite.pl | 50 +++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 src/test/modules/test_autovacuum/t/002_reltuples_nonfinite.pl diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 45abf48768a..324c281dbf4 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -3317,7 +3317,15 @@ relation_needs_vacanalyze(Oid relid, scores->anl = (double) anltuples / Max(anlthresh, 1); scores->anl *= autovacuum_analyze_score_weight; scores->max = Max(scores->max, scores->anl); - if (av_enabled && anltuples > anlthresh) + + /* + * Analyze when enough tuples have changed. Force it too when + * reltuples is not finite: an infinity or NaN (e.g. from a bad + * statistics import) propagates into anlthresh, so "anltuples > + * anlthresh" never holds and the bogus value would persist. + */ + if (av_enabled && + (anltuples > anlthresh || !isfinite(reltuples))) *doanalyze = true; } diff --git a/src/test/modules/test_autovacuum/meson.build b/src/test/modules/test_autovacuum/meson.build index 86e392bc0de..78c1d002ba2 100644 --- a/src/test/modules/test_autovacuum/meson.build +++ b/src/test/modules/test_autovacuum/meson.build @@ -10,6 +10,7 @@ tests += { }, 'tests': [ 't/001_parallel_autovacuum.pl', + 't/002_reltuples_nonfinite.pl', ], }, } diff --git a/src/test/modules/test_autovacuum/t/002_reltuples_nonfinite.pl b/src/test/modules/test_autovacuum/t/002_reltuples_nonfinite.pl new file mode 100644 index 00000000000..36dbb22fe40 --- /dev/null +++ b/src/test/modules/test_autovacuum/t/002_reltuples_nonfinite.pl @@ -0,0 +1,50 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Verify that autoanalyze recovers a non-finite pg_class.reltuples. Such a +# value (infinity or NaN) makes the analyze threshold non-finite, so without +# special handling autoanalyze would never run and the bogus value could +# never be corrected. relation_needs_vacanalyze() forces an analyze in that +# case, and ANALYZE recomputes reltuples from a fresh sample. + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->append_conf( + 'postgresql.conf', qq{ +autovacuum = on +autovacuum_naptime = '1s' +allow_system_table_mods = on +}); +$node->start; + +# Start with a table that has finite, correct statistics. +$node->safe_psql( + 'postgres', qq{ + CREATE TABLE t (a int); + INSERT INTO t SELECT generate_series(1, 1000); + ANALYZE t; +}); + +# Introduce an infinite reltuples value, mimicking a corrupted pg_class row or +# a bad statistics import. +$node->safe_psql('postgres', + "UPDATE pg_class SET reltuples = 'infinity'::float4 WHERE oid = 't'::regclass" +); + +# Wait for autoanalyze to restore reltuples to a finite value. +ok( $node->poll_query_until( + 'postgres', qq{ + SELECT reltuples >= 0 AND reltuples < 'infinity'::float4 + FROM pg_class WHERE oid = 't'::regclass + }), + "autoanalyze recomputed a non-finite reltuples to a finite value"); + +$node->stop; + +done_testing(); -- 2.47.3