Re: pg_class.reltuples can become non-finite and never recovers

From: Jan Nidzwetzki <jan(at)planetscale(dot)com>
To: Tomas Vondra <tomas(at)vondra(dot)me>
Cc: pgsql-hackers(at)lists(dot)postgresql(dot)org
Subject: Re: pg_class.reltuples can become non-finite and never recovers
Date: 2026-07-28 12:47:26
Message-ID: 9E5188D0-8177-480D-8453-5219AD2AA189@planetscale.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

Hello Tomas,

> On 24. Jul 2026, at 19:50, Tomas Vondra <tomas(at)vondra(dot)me> wrote:

[…]

>>>> 3. Infinity defeats autovacuum's self-healing
>>>> ----------------------------------------------
>>>>
>>>> Normally, a wrong reltuples is self-correcting: ANALYZE recomputes it
>>>> from a fresh sample. But a non-finite reltuples makes the analyze
>>>> threshold non-finite as well:
>>>>
>>>> anlthresh = analyze_threshold + analyze_scale_factor * reltuples
>>>>
>>>> so in relation_needs_vacanalyze() the test "anltuples > anlthresh" can
>>>> never be true, autoanalyze never fires, and the bad value is stuck
>>>> forever and the relation never recovers on its own.
>>>>
>>>> The second attached patch makes autovacuum force an analyze when
>>>> reltuples is not finite, so a corrupted value heals itself on the next
>>>> autovacuum cycle. It includes a TAP test that plants an infinite
>>>> reltuples and checks that autoanalyze restores a finite value.
>>>>
>>>
>>> I'm skeptical about this change. Yeah, it'd probably help in this
>>> particular Infinity case, but it can be bogus in many other ways. We
>>> should really be careful to not put bogus values into reltuples.
>>>
>>
>> I experimented with this a bit more, but I'm rather skeptical.
>>
>> I understand the desire to self-heal, but I don't think we do that
>> elsewhere. We assume the data is correct. I'm sure we had a bunch of
>> data corruption bugs, and I don't quite see why this particular case
>> would be any different.

[…]

> Oh, one more thing - I'm trying how to figure out how widespread this
> issue could be, which I guess could affect how far we want to go.
>
> I did a lot of experiments, and I only ever saw the field initialized to
> 0 (so it produced the right result). Maybe I'm just lucky, but more
> likely it depends on platform / kernel version / compiler? What do I
> need to do to actually see a garbage value?
>
> In fact, not even valgrind reports the access as invalid. I guess that's
> one reason why I missed the issue when working on the feature :-/

It is also quite hard for me to reproduce it in a controlled environment.
I don't have a reliable method to reproduce it from the SQL level in a
way that writes truly random values into the catalog. The best I have
puts a wrong 0 (instead of -1) into the catalog for the parallel build
(Debian 13, aarch64, gcc (Debian 14.2.0-19)). Note the serial build
gets -1 while the parallel build gets 0:

-+-
-- serial build
SET min_parallel_table_scan_size = 0;
SET max_parallel_maintenance_workers = 0;
CREATE TABLE serial_build (ts tsvector);
CREATE INDEX ON serial_build USING gin (ts);

-- parallel build
SET max_parallel_maintenance_workers = 4;
CREATE TABLE parallel_build (ts tsvector);
CREATE INDEX ON parallel_build USING gin (ts);

-- different catalog entries
SELECT relname, reltuples FROM pg_class
WHERE relname IN ('serial_build','parallel_build');

relname | reltuples
----------------+-----------
serial_build | -1
parallel_build | 0
(2 rows)
-+-

I was hoping to get a cleaner reproducer by building PostgreSQL with
'-ftrivial-auto-var-init=pattern'. Unfortunately, both Clang's 0xAA and
GCC's 0xFE fill patterns represent negative doubles, so the check

update_stats = reltuples >= 0 && !IsBinaryUpgrade;

in index_update_stats() discards them before they reach the catalog,
and there is no way to configure a positive fill. With that build, the
reproducer above just yields -1 for both tables:

-+-
relname | reltuples
----------------+-----------
serial_build | -1
parallel_build | -1
(2 rows)
-+-

You can still confirm the uninitialized read directly with that build:
set a breakpoint in index_update_stats() and inspect reltuples. The
debugger shows it holding the negative fill-pattern value, which is
then rejected by the reltuples >= 0 check above, leaving the
pre-existing -1.

Best regards
Jan

---
Jan Nidzwetzki
PlanetScale Postgres Core Team

In response to

Browse pgsql-hackers by date

  From Date Subject
Next Message Rafia Sabih 2026-07-28 13:01:27 Re: [PATCH] Add tests for src/backend/nodes/extensible.c
Previous Message Tom Lane 2026-07-28 12:24:25 Re: remove_useless_joins vs. bug #19560