pg_restore_attribute_stats() accepts non-finite values

From: Ewan Young <kdbase(dot)hack(at)gmail(dot)com>
To: PostgreSQL Hackers <pgsql-hackers(at)lists(dot)postgresql(dot)org>
Cc: Tomas Vondra <tomas(dot)vondra(at)postgresql(dot)org>, Jan Nidzwetzki <jan(at)planetscale(dot)com>, Jeff Davis <pgsql(at)j-davis(dot)com>, Corey Huinker <corey(dot)huinker(at)gmail(dot)com>
Subject: pg_restore_attribute_stats() accepts non-finite values
Date: 2026-08-25 11:36:04
Message-ID: CAON2xHMSxnUqyiQF3LsEeC0d7jq2Y-w8qLT5kHqANMqpDONNnw@mail.gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

Hi,

7cb9060dcde taught pg_restore_relation_stats() to reject a non-finite
reltuples, on the grounds that Infinity and NaN pass the existing range
check and then get stored and used verbatim. pg_restore_attribute_stats()
has the same gap for its float arguments, which that thread did not cover.

CREATE TABLE t (a int);
INSERT INTO t SELECT g FROM generate_series(1, 1000) g;
ANALYZE t;

SELECT pg_restore_attribute_stats('schemaname', 'public', 'relname', 't',
'attname', 'a', 'inherited', false,
'null_frac', 'NaN'::real, 'n_distinct', 'Infinity'::real);
t

SELECT stanullfrac, stadistinct FROM pg_statistic
WHERE starelid = 't'::regclass;
NaN | Infinity

The values are stored, and the planner does not defend against them.
CLAMP_PROBABILITY() is two comparisons, both false for NaN, so it does not
neutralise a non-finite value the way it clamps an out-of-range finite one.
The effect is visible immediately:

-- with the NaN null_frac above:
EXPLAIN SELECT * FROM t WHERE a = 5;
Seq Scan on t (cost=0.00..17.50 rows=10000000000...000 width=4)

A non-finite n_distinct or correlation is worse than a bad row estimate: a
NaN correlation puts a literal "cost=0.29..NaN" on an index scan, which
then takes part in path cost comparisons.

This only comes in through the restore path -- ANALYZE never produces a
non-finite value, even for a column that itself contains Infinity/NaN,
since the stats are frequencies and ratios rather than the data. So the
realistic trigger is a corrupt or cross-version dump fed through
pg_restore_attribute_stats(), and once stored the value survives until the
next ANALYZE.

Patch attached. It rejects non-finite values for the scalar arguments
null_frac, n_distinct, correlation and range_empty_frac, and for the
float4[] arguments most_common_freqs, most_common_elem_freqs and
elem_count_histogram, dropping the bad value with a WARNING as the other
non-fatal checks do and letting the rest of the import proceed. The two
new checks live in stat_utils.c alongside the existing ones.

Two things I decided deliberately, happy to be overruled:

- A negative n_distinct encodes a distinct-value ratio rather than a
count, so it is still accepted, matching the -1.0 special case kept for
reltuples.

- These functions do only superficial validation by design (per
ce207d2a790), so I did not add range checks for finite-but-bogus values;
the planner does clamp those. This only closes the non-finite hole,
which the planner cannot.

--
Regards,
Ewan Young

Attachment Content-Type Size
v1-0001-Reject-non-finite-values-when-restoring-attribute-st.patch application/octet-stream 12.3 KB

Browse pgsql-hackers by date

  From Date Subject
Next Message Andrew Dunstan 2026-08-25 11:37:48 Re: Add a Nix flake
Previous Message 贾明伟 2026-08-25 11:31:22 [RFC] Umbra: physical remapping, protection mechanisms, and write amplification