BUG #19821: pg_restore_attribute_stats() accepts an unsorted scalar histogram_bounds, producing badly wrong row

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 #19821: pg_restore_attribute_stats() accepts an unsorted scalar histogram_bounds, producing badly wrong row
Date: 2026-10-30 07:01:30
Message-ID: 19821-0aa4084a3c6d9899@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: 19821
Logged by: chunling qin
Email address: 303677365(at)qq(dot)com
PostgreSQL version: 18.6
Operating system: Linux (TencentOS 4.4, x86_64)
Description:

Hello,

pg_restore_attribute_stats() validates that range_bounds_histogram is
sorted in ascending order (stat_utils.c:842, commit df293aed46e "Reject
incorrect range_bounds_histograms in stats import functions"). The very
same function does not apply any ordering check to the scalar
histogram_bounds, even though the consumer, histogram_selectivity()
(src/backend/utils/adt/selfuncs.c), binary-searches the histogram and
therefore assumes ascending order. So an unsorted (e.g. descending)
histogram_bounds is accepted -- the function returns true -- and every
row estimate made from it afterwards is arbitrary.

Reproducer (self-contained):

CREATE TABLE h1 (a int) WITH (autovacuum_enabled = false);
INSERT INTO h1 SELECT g FROM generate_series(1, 100) g;
ANALYZE h1;

-- baseline: sorted histogram built by ANALYZE
EXPLAIN SELECT * FROM h1 WHERE a < 50;
-- Seq Scan ... rows=48 (true value 49)

-- import a DESCENDING histogram: accepted, returns true
SELECT pg_restore_attribute_stats(
'schemaname', 'public', 'relname', 'h1',
'attname', 'a', 'inherited', false,
'histogram_bounds', '{100,90,80,70,60,50,40,30,20,10,1}');
-- t

EXPLAIN SELECT * FROM h1 WHERE a < 50; -- rows=1 (true value 49)
EXPLAIN SELECT * FROM h1 WHERE a < 95; -- rows=100 (true value 94)
EXPLAIN SELECT * FROM h1 WHERE a > 95; -- rows=1 (true value 5)

For comparison, in the very same function and session, the range
variant of the same mistake is properly rejected:

SELECT pg_restore_attribute_stats(
'schemaname', 'public', 'relname', 'h1',
'attname', 'a', 'inherited', false,
'range_bounds_histogram', '{"[90,93)","[1,4)","[50,53)"}');
-- WARNING: "range_bounds_histogram" must have its lower and upper
-- bounds sorted in ascending order

Expected: histogram_bounds should get the same ordering check that
range_bounds_histogram already has (an unsorted input should produce a
WARNING and be skipped, not silently accepted). The sibling slots
most_common_elems and elem_count_histogram also carry ordering/shape
contracts from ANALYZE that the import side does not verify, but the
scalar histogram is the one with a directly measurable effect on row
estimates shown above.

Actual: the function returns true and the planner's estimates become
arbitrary (49 -> 1, 94 -> 100, 5 -> 1).

Reproduced on:
- PostgreSQL 20devel (snapshot of 2026-10-27, x86_64-pc-linux-gnu)
- PostgreSQL 18.6 (REL_18_STABLE, self-built)

A draft fix location would be the STATISTIC_KIND_HISTOGRAM branch of
pg_restore_attribute_stats() (src/backend/statistics/attribute_stats.c,
around lines 425-440): apply the same ascending-order check as
statatt_check_bounds_histogram() (src/backend/statistics/stat_utils.c).

Regards,
Chunling Qin

Responses

Browse pgsql-bugs by date

  From Date Subject
Next Message Richard Guo 2026-10-30 07:24:55 Re: Wrong results: hashed SubPlan referenced twice after OR-qual extraction reuses a stale hash table (13 to 19beta4)
Previous Message PG Bug reporting form 2026-10-30 06:58:56 BUG #19820: ANALYZE can store empty (0-dimensional) stavalues that the statistics import functions refuse to res