| 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`.
| 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 - |