BUG #19842: pg_restore_extended_stats() rejects a faithful pg_dump of ANALYZE-produced range expression statisti

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 #19842: pg_restore_extended_stats() rejects a faithful pg_dump of ANALYZE-produced range expression statisti
Date: 2026-11-07 04:22:06
Message-ID: 19842-ca042326dd2430a4@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: 19842
Logged by: chunling qin
Email address: 303677365(at)qq(dot)com
PostgreSQL version: 19rc1
Operating system: Linux x86_64; platform-independent (reproduced on
Description:

**Description:**

When an extended-statistics object has an expression of a range/multirange
type, and fewer than 2 non-empty range values are sampled by ANALYZE (a
2-row table, or a column where most ranges are empty),
`compute_range_stats()` still produces a statistics slot whose only range
field is `range_empty_frac`. This is legal and intentional:
`pg_stats_ext_exprs` exposes it, and `pg_dump --statistics-only` therefore
emits a `pg_restore_extended_stats()` call whose `exprs` JSON contains only
`range_empty_frac` (no `range_length_histogram`, no
`range_bounds_histogram`).

The restore side rejects exactly this input with a WARNING (and returns
false), so on restore/pg_upgrade the expression statistics are silently
lost. The planner then has no `range_empty_frac` and wrongfully assumes "no
empty ranges", producing wrong selectivity/row estimates.

By the rule established by the statistics-import series (commit 142fd8ff13d
and its series — "the source and the target should hold the same stats data
after restore"), a restore function must accept the statistics that ANALYZE
itself can produce; this is a case that series does not cover.

**Reproduction (self-contained SQL; verified on master):**

```sql
-- source side: 2-row table, range expression with < 2 non-empty values
DROP TABLE IF EXISTS t;
CREATE TABLE t (id int, r int4range);
INSERT INTO t VALUES (1, int4range(1,5)), (2, 'empty'::int4range);
CREATE STATISTICS stx ON (r * int4range(1,10)) FROM t;
ANALYZE t;

-- ANALYZE legitimately produced ONLY range_empty_frac (0.5):
SELECT e.range_length_histogram, e.range_empty_frac,
e.range_bounds_histogram
FROM pg_stats_ext_exprs e
WHERE e.statistics_name='stx' AND NOT e.inherited;
-- (NULL) | 0.5 | (NULL)

-- what pg_dump --statistics-only emits for this object (replayed verbatim):
SELECT
pg_catalog.pg_clear_extended_stats('public','t','public','stx',false);
SELECT * FROM pg_catalog.pg_restore_extended_stats(
'version', '200000'::integer,
'schemaname', 'public',
'relname', 't',
'statistics_schemaname', 'public',
'statistics_name', 'stx',
'inherited', 'f'::boolean,
'exprs', '[{"avg_width": "13","null_frac": "0","n_distinct": "-1",
"range_empty_frac": "0.5"}]'::jsonb
);
-- WARNING: ... (rejected, returns false; restore continues, statistics
lost)

-- after restore: the statistics are gone (expected: range_empty_frac =
0.5):
SELECT e.range_length_histogram, e.range_empty_frac,
e.range_bounds_histogram
FROM pg_stats_ext_exprs e
WHERE e.statistics_name='stx' AND NOT e.inherited;
-- (NULL) | (NULL) | (NULL) <- lost
```

**Actual results:** `pg_restore_extended_stats()` raises a WARNING and
returns false for the exact `exprs` JSON that pg_dump emits from this legal
ANALYZE output; the restored cluster has no `range_empty_frac`.

**Expected behavior:** the restore function should accept a stats slot whose
only range field is `range_empty_frac` (equivalently: treat missing
histogram arrays the same way `compute_range_stats()` does — an empty array
means "no histogram"), restoring `range_empty_frac = 0.5`.

Browse pgsql-bugs by date

  From Date Subject
Next Message PG Bug reporting form 2026-11-07 14:12:16 BUG #19843: decode(..., 'base64url') returns an extra byte for "ab="
Previous Message PG Bug reporting form 2026-11-07 04:12:26 BUG #19841: SQL/JSON RETURNING coercion to GUC-dependent types (timestamptz etc.) is wrongly judged IMMUTABLE -