BUG #19814: pg_restore_attribute_stats: MCV accepted without eq operator; ANALYZE range slot rejected

From: PG Bug reporting form <noreply(at)postgresql(dot)org>
To: pgsql-bugs(at)lists(dot)postgresql(dot)org
Cc: 303677365(at)qq(dot)com
Subject: BUG #19814: pg_restore_attribute_stats: MCV accepted without eq operator; ANALYZE range slot rejected
Date: 2026-10-29 07:00:28
Message-ID: 19814-5005fc464c4ef1ea@postgresql.org
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-bugs

The following bug has been logged on the website:

Bug reference: 19814
Logged by: chunling qin
Email address: 303677365(at)qq(dot)com
PostgreSQL version: 18.6
Operating system: Linux x86_64
Description:

Two defects in pg_restore_attribute_stats(), in opposite directions: one
check is missing (invalid statistics are accepted), one check is too strict
(statistics that ANALYZE itself produces are rejected). Both reproduce on
the official postgres:latest docker image (18.6).

1. MCV statistics are accepted for types with no default equality operator

CREATE TABLE jt (j json, p point) WITH (autovacuum_enabled = false);
INSERT INTO jt SELECT ('{"k":'||(g%5)||'}')::json, point(g%5,g%5) FROM
generate_series(1,300) g;
ANALYZE jt;

ANALYZE produces no statistics slot for these columns (stakind1 = 0 - no
default btree/hash opclass). Restoring MCV values is nevertheless accepted:

SELECT pg_restore_attribute_stats(
'schemaname', current_schema()::text, 'relname', 'jt', 'attname', 'j',
'inherited', false,
'most_common_vals', '{"1","2"}'::text, 'most_common_freqs',
'{0.4,0.4}'::real[]);
-> true (no warning)

The result is a STATISTIC_KIND_MCV slot (stakind1 = 1) with staop1 = 0
(InvalidOid) - a state ANALYZE can never produce - and pg_stats now reports
most_common_vals for the column.

The sibling checks in the same function behave correctly on the same column:

histogram_bounds -> WARNING: could not determine less-than operator
... -> false
most_common_elems -> WARNING: could not determine element type ...
-> false

Only the MCV path lacks the equality-operator (eq_opr) guard.

2. The range_empty_frac-only slot that ANALYZE produces cannot be restored

CREATE TABLE w1 (r int4range) WITH (autovacuum_enabled = false);
INSERT INTO w1 SELECT 'empty'::int4range FROM generate_series(1,20000)
g;
ANALYZE w1;

ANALYZE produces a STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM slot with
stavalues1 = NULL (no length histogram; the sample has fewer than two
non-empty ranges) and stanumbers1 = {1} (the empty fraction):

SELECT stakind1, stavalues1 IS NULL AS vals_null, stanumbers1
FROM pg_statistic WHERE starelid = 'w1'::regclass;
-> stakind1 = 6, vals_null = t, stanumbers1 = {1}

pg_restore_attribute_stats() requires range_length_histogram to be specified
whenever range_empty_frac is, so restoring exactly the values ANALYZE
produced fails:

SELECT pg_restore_attribute_stats(
'schemaname', current_schema()::text, 'relname', 'w1', 'attname', 'r',
'inherited', false,
'null_frac', <the analyzed value>, 'avg_width', <...>, 'n_distinct',
<...>,
'range_empty_frac', <the analyzed value>);
WARNING: argument "range_length_histogram" must be specified when
argument "range_empty_frac" is specified
-> false

and the statistics row ends up empty - the slot is lost. Restoring the same
row without the range arguments succeeds (returns true), confirming that the
range pairing rule is what rejects it. The export/restore round trip
therefore loses statistics that ANALYZE itself computed - for example in the
pre-ANALYZE step of an upgrade, where the target has no statistics row yet.

Note: when passing these arguments, use values that are already of type real
(e.g. carried through a table or a subquery); untyped numeric literals raise
unrelated "has type numeric, expected type real" warnings.

Responses

Browse pgsql-bugs by date

  From Date Subject
Next Message John Naylor 2026-10-29 08:37:37 spg_quad_inner_consistent() doesn't check all four box corners
Previous Message PG Bug reporting form 2026-10-29 06:51:13 BUG #19813: jsonpath: datetime errors bypass silent/ON ERROR; boolean() inconsistent; PASSING rejects bpchar/nam