1
0
mirror of https://github.com/postgres/postgres.git synced 2026-10-23 13:29:09 +03:00

Fix import of statistics for domains over [multi]range types and tsvector

pg_restore_attribute_stats() rejected range_length_histogram,
range_empty_frac and range_bounds_histogram for a column whose type is a
domain over a range or a multirange type.  The same issue existed for
most_common_elems and elem_count_histogram for a domain over tsvector.
pg_restore_extended_stats() had the same set of problems for expressions
whose type is a domain over a range, a multirange type, or tsvector.

ANALYZE is able to generate such stats, incorporating the knowledge to
handle domains in the in-core typanalyze callbacks.

This problem is resolved by being more aggressive with the fetch of the
base type of a domain in the early phases of restore for attribute and
extended stats (special tip to Jian He for pointing out the unnecessary
typcache lookups), reflecting on the surrounding helper routines shared
by both code paths.  The fix for v18 is local to attribute_stats.c, as
only attribute stats need to be touched.

Tests are included in a fashion consistent with the surroundings.  The
consequence of this issue was the rejection of stats that ANALYZE was
able to build, which was not critical but annoying as it would lead to a
gap in the stats restored.

Reported-by: Qifan Liu <imchifan@163.com>
Author: Michael Paquier <michael@paquier.xyz>
Reviewed-by: Jian He <jian.universality@gmail.com>
Reviewed-by: Corey Huinker <corey.huinker@gmail.com>
Discussion: https://postgr.es/m/19715-b8be35083016f289@postgresql.org
Backpatch-through: 18
This commit is contained in:
Michael Paquier
2026-09-26 09:28:18 +09:00
parent b331a2c84c
commit 753057e340
3 changed files with 327 additions and 26 deletions

View File

@@ -104,10 +104,12 @@ static bool attribute_statistics_update(FunctionCallInfo fcinfo);
static Node *get_attr_expr(Relation rel, int attnum);
static void get_attr_stat_type(Oid reloid, AttrNumber attnum,
Oid *atttypid, int32 *atttypmod,
char *atttyptype, Oid *atttypcoll,
TypeCacheEntry **basetypcache, Oid *atttypcoll,
Oid *eq_opr, Oid *lt_opr);
static bool get_elem_stat_type(Oid atttypid, char atttyptype,
static bool get_elem_stat_type(TypeCacheEntry *basetypcache,
Oid *elemtypid, Oid *elem_eq_opr);
static bool get_range_stat_type(TypeCacheEntry *basetypcache,
Oid *rangetypid);
static Datum text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d,
Oid typid, int32 typmod, bool *ok);
static void set_stats_slot(Datum *values, bool *nulls, bool *replaces,
@@ -152,7 +154,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
Oid atttypid = InvalidOid;
int32 atttypmod;
char atttyptype;
TypeCacheEntry *basetypcache;
Oid atttypcoll = InvalidOid;
Oid eq_opr = InvalidOid;
Oid lt_opr = InvalidOid;
@@ -160,6 +162,8 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
Oid elemtypid = InvalidOid;
Oid elem_eq_opr = InvalidOid;
Oid bounds_typid = InvalidOid;
FmgrInfo array_in_fn;
bool do_mcv = !PG_ARGISNULL(MOST_COMMON_FREQS_ARG) &&
@@ -290,14 +294,13 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
/* derive information from attribute */
get_attr_stat_type(reloid, attnum,
&atttypid, &atttypmod,
&atttyptype, &atttypcoll,
&basetypcache, &atttypcoll,
&eq_opr, &lt_opr);
/* if needed, derive element type */
if (do_mcelem || do_dechist)
{
if (!get_elem_stat_type(atttypid, atttyptype,
&elemtypid, &elem_eq_opr))
if (!get_elem_stat_type(basetypcache, &elemtypid, &elem_eq_opr))
{
ereport(WARNING,
(errmsg("could not determine element type of column \"%s\"", attname),
@@ -328,7 +331,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
/* only range types can have range stats */
if ((do_range_length_histogram || do_bounds_histogram) &&
!(atttyptype == TYPTYPE_RANGE || atttyptype == TYPTYPE_MULTIRANGE))
!get_range_stat_type(basetypcache, &bounds_typid))
{
ereport(WARNING,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
@@ -492,14 +495,8 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
{
bool converted = false;
Datum stavalues;
Oid bounds_typid = atttypid;
/*
* If it's a multirange, step down to the range type, as is done by
* multirange_typanalyze().
*/
if (type_is_multirange(atttypid))
bounds_typid = get_multirange_range(atttypid);
Assert(OidIsValid(bounds_typid));
stavalues = text_to_stavalues("range_bounds_histogram",
&array_in_fn,
@@ -598,18 +595,20 @@ get_attr_expr(Relation rel, int attnum)
/*
* Derive type information from the attribute.
*
* *atttypid and *atttypmod describe the type as declared. *basetypcache is
* the cache entry of the base type behind any domain.
*/
static void
get_attr_stat_type(Oid reloid, AttrNumber attnum,
Oid *atttypid, int32 *atttypmod,
char *atttyptype, Oid *atttypcoll,
TypeCacheEntry **basetypcache, Oid *atttypcoll,
Oid *eq_opr, Oid *lt_opr)
{
Relation rel = relation_open(reloid, AccessShareLock);
Form_pg_attribute attr;
HeapTuple atup;
Node *expr;
TypeCacheEntry *typcache;
atup = SearchSysCache2(ATTNUM, ObjectIdGetDatum(reloid),
Int16GetDatum(attnum));
@@ -656,16 +655,22 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum,
ReleaseSysCache(atup);
/* finds the right operators even if atttypid is a domain */
typcache = lookup_type_cache(*atttypid, TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR);
*atttyptype = typcache->typtype;
*eq_opr = typcache->eq_opr;
*lt_opr = typcache->lt_opr;
*basetypcache = lookup_type_cache(*atttypid, TYPECACHE_LT_OPR |
TYPECACHE_EQ_OPR |
TYPECACHE_DOMAIN_BASE_INFO);
if (OidIsValid((*basetypcache)->domainBaseType))
*basetypcache = lookup_type_cache((*basetypcache)->domainBaseType,
TYPECACHE_LT_OPR |
TYPECACHE_EQ_OPR);
*eq_opr = (*basetypcache)->eq_opr;
*lt_opr = (*basetypcache)->lt_opr;
/*
* Special case: collation for tsvector is DEFAULT_COLLATION_OID. See
* compute_tsvector_stats().
*/
if (*atttypid == TSVECTOROID)
if ((*basetypcache)->type_id == TSVECTOROID)
*atttypcoll = DEFAULT_COLLATION_OID;
relation_close(rel, NoLock);
@@ -673,14 +678,17 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum,
/*
* Derive element type information from the attribute type.
*
* The type cache entry should be derived from a previous call to
* get_attr_stat_type().
*/
static bool
get_elem_stat_type(Oid atttypid, char atttyptype,
get_elem_stat_type(TypeCacheEntry *basetypcache,
Oid *elemtypid, Oid *elem_eq_opr)
{
TypeCacheEntry *elemtypcache;
if (atttypid == TSVECTOROID)
if (basetypcache->type_id == TSVECTOROID)
{
/*
* Special case: element type for tsvector is text. See
@@ -690,8 +698,8 @@ get_elem_stat_type(Oid atttypid, char atttyptype,
}
else
{
/* find underlying element type through any domain */
*elemtypid = get_base_element_type(atttypid);
/* find the underlying element type */
*elemtypid = get_element_type(basetypcache->type_id);
}
if (!OidIsValid(*elemtypid))
@@ -707,6 +715,33 @@ get_elem_stat_type(Oid atttypid, char atttyptype,
return true;
}
/*
* Derive the range type to use from the attribute type, returning false if
* the attribute cannot have range statistics at all.
*
* For a multirange type, we step down to its range type, because
* compute_range_stats() stores range bounds even when analyzing a multirange
* column (see also range_typanalyze() and multirange_typanalyze()).
*
* The type cache entry should be derived from a previous call to
* get_attr_stat_type(), so that any domain has already been looked through.
*/
static bool
get_range_stat_type(TypeCacheEntry *basetypcache, Oid *rangetypid)
{
if (basetypcache->typtype == TYPTYPE_MULTIRANGE)
*rangetypid = get_multirange_range(basetypcache->type_id);
else if (basetypcache->typtype == TYPTYPE_RANGE)
*rangetypid = basetypcache->type_id;
else
{
*rangetypid = InvalidOid;
return false;
}
return true;
}
/*
* Cast a text datum into an array with element type elemtypid.
*

View File

@@ -1273,6 +1273,153 @@ SELECT pg_catalog.pg_restore_attribute_stats(
t
(1 row)
-- test for domains over range and multirange types
CREATE DOMAIN stats_import.dom_int4 AS int4;
CREATE DOMAIN stats_import.dom_range AS int4range;
CREATE DOMAIN stats_import.dom_mrange AS int4multirange;
CREATE TABLE stats_import.test_dom(
id stats_import.dom_int4,
drange stats_import.dom_range,
dmrange stats_import.dom_mrange
) WITH (autovacuum_enabled = false);
INSERT INTO stats_import.test_dom
VALUES (1, '[1,3)', '{[1,3),[5,9),[20,30)}'),
(2, '[5,9)', '{[11,13),[15,19),[20,30)}'),
(3, '[11,15)', '{[21,23),[25,29),[120,130)}');
-- warn: domain over a scalar type cannot have range stats
SELECT pg_catalog.pg_restore_attribute_stats(
'schemaname', 'stats_import',
'relname', 'test_dom',
'attname', 'id',
'inherited', false,
'null_frac', 0.25::real,
'range_length_histogram', '{2,4,4}'::text,
'range_empty_frac', '0'::real,
'range_bounds_histogram', '{"[1,3)","[5,9)","[11,15)"}'::text
);
WARNING: column "id" is not a range type
DETAIL: Cannot set STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM or STATISTIC_KIND_BOUNDS_HISTOGRAM.
pg_restore_attribute_stats
----------------------------
f
(1 row)
SELECT *
FROM pg_stats
WHERE schemaname = 'stats_import'
AND tablename = 'test_dom'
AND inherited = false
AND attname = 'id';
schemaname | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram
--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+------------------------
stats_import | test_dom | id | f | 0.25 | 0 | 0 | | | | | | | | | |
(1 row)
-- ok: range stats for a domain over a range type
SELECT pg_catalog.pg_restore_attribute_stats(
'schemaname', 'stats_import',
'relname', 'test_dom',
'attname', 'drange',
'inherited', false,
'range_length_histogram', '{2,4,4}'::text,
'range_empty_frac', '0'::real,
'range_bounds_histogram', '{"[1,3)","[5,9)","[11,15)"}'::text
);
pg_restore_attribute_stats
----------------------------
t
(1 row)
SELECT *
FROM pg_stats
WHERE schemaname = 'stats_import'
AND tablename = 'test_dom'
AND inherited = false
AND attname = 'drange';
schemaname | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram
--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+-----------------------------
stats_import | test_dom | drange | f | 0 | 0 | 0 | | | | | | | | {2,4,4} | 0 | {"[1,3)","[5,9)","[11,15)"}
(1 row)
-- ok: range stats for a domain over a multirange type.
SELECT pg_catalog.pg_restore_attribute_stats(
'schemaname', 'stats_import',
'relname', 'test_dom',
'attname', 'dmrange',
'inherited', false,
'range_length_histogram', '{29,29,109}'::text,
'range_empty_frac', '0'::real,
'range_bounds_histogram', '{"[1,30)","[11,30)","[21,130)"}'::text
);
pg_restore_attribute_stats
----------------------------
t
(1 row)
SELECT *
FROM pg_stats
WHERE schemaname = 'stats_import'
AND tablename = 'test_dom'
AND inherited = false
AND attname = 'dmrange';
schemaname | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram
--------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------+------------------------+------------------+---------------------------------
stats_import | test_dom | dmrange | f | 0 | 0 | 0 | | | | | | | | {29,29,109} | 0 | {"[1,30)","[11,30)","[21,130)"}
(1 row)
-- warn: multirange values in the bounds histogram of a domain. These
-- must be ranges.
SELECT pg_catalog.pg_restore_attribute_stats(
'schemaname', 'stats_import',
'relname', 'test_dom',
'attname', 'dmrange',
'inherited', false,
'range_length_histogram', '{29,29,109}'::text,
'range_empty_frac', '0'::real,
'range_bounds_histogram', '{"{[1,30)}","{[11,30)}"}'::text
);
WARNING: malformed range literal: "{[1,30)}"
DETAIL: Missing left parenthesis or bracket.
pg_restore_attribute_stats
----------------------------
f
(1 row)
-- test for a domain over tsvector.
CREATE DOMAIN stats_import.dom_tsvector AS tsvector;
CREATE TABLE stats_import.test_dom_ts(
id int,
v stats_import.dom_tsvector
) WITH (autovacuum_enabled = false);
INSERT INTO stats_import.test_dom_ts
SELECT g, to_tsvector('english', 'the quick brown fox ' || g)
FROM generate_series(1, 20) AS g;
-- ok: mcelem and elem_count_histogram for a domain over tsvector
SELECT pg_catalog.pg_restore_attribute_stats(
'schemaname', 'stats_import',
'relname', 'test_dom_ts',
'attname', 'v',
'inherited', false,
'most_common_elems', '{brown,fox,quick}'::text,
'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
'elem_count_histogram', '{4,4,4,4,4,4,4,4,4,4}'::real[]
);
pg_restore_attribute_stats
----------------------------
t
(1 row)
SELECT *
FROM pg_stats
WHERE schemaname = 'stats_import'
AND tablename = 'test_dom_ts'
AND inherited = false
AND attname = 'v';
schemaname | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram | range_length_histogram | range_empty_frac | range_bounds_histogram
--------------+-------------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+-----------------------+------------------------+------------------+------------------------
stats_import | test_dom_ts | v | f | 0 | 0 | 0 | | | | | {brown,fox,quick} | {0.3,0.2,0.2,0.3,0} | {4,4,4,4,4,4,4,4,4,4} | | |
(1 row)
--
-- Test the ability to exactly copy data from one table to an identical table,
-- correctly reconstructing the stakind order as well as the staopN and
@@ -1549,11 +1696,17 @@ AND attname = 'i';
DROP TABLE stats_temp;
DROP SCHEMA stats_import CASCADE;
NOTICE: drop cascades to 7 other objects
NOTICE: drop cascades to 13 other objects
DETAIL: drop cascades to type stats_import.complex_type
drop cascades to table stats_import.test
drop cascades to table stats_import.test_mr
drop cascades to table stats_import.part_parent
drop cascades to sequence stats_import.testseq
drop cascades to view stats_import.testview
drop cascades to type stats_import.dom_int4
drop cascades to type stats_import.dom_range
drop cascades to type stats_import.dom_mrange
drop cascades to table stats_import.test_dom
drop cascades to type stats_import.dom_tsvector
drop cascades to table stats_import.test_dom_ts
drop cascades to table stats_import.test_clone

View File

@@ -882,6 +882,119 @@ SELECT pg_catalog.pg_restore_attribute_stats(
'range_bounds_histogram', '{"[1,30)","[11,30)","[21,130)"}'::text
);
-- test for domains over range and multirange types
CREATE DOMAIN stats_import.dom_int4 AS int4;
CREATE DOMAIN stats_import.dom_range AS int4range;
CREATE DOMAIN stats_import.dom_mrange AS int4multirange;
CREATE TABLE stats_import.test_dom(
id stats_import.dom_int4,
drange stats_import.dom_range,
dmrange stats_import.dom_mrange
) WITH (autovacuum_enabled = false);
INSERT INTO stats_import.test_dom
VALUES (1, '[1,3)', '{[1,3),[5,9),[20,30)}'),
(2, '[5,9)', '{[11,13),[15,19),[20,30)}'),
(3, '[11,15)', '{[21,23),[25,29),[120,130)}');
-- warn: domain over a scalar type cannot have range stats
SELECT pg_catalog.pg_restore_attribute_stats(
'schemaname', 'stats_import',
'relname', 'test_dom',
'attname', 'id',
'inherited', false,
'null_frac', 0.25::real,
'range_length_histogram', '{2,4,4}'::text,
'range_empty_frac', '0'::real,
'range_bounds_histogram', '{"[1,3)","[5,9)","[11,15)"}'::text
);
SELECT *
FROM pg_stats
WHERE schemaname = 'stats_import'
AND tablename = 'test_dom'
AND inherited = false
AND attname = 'id';
-- ok: range stats for a domain over a range type
SELECT pg_catalog.pg_restore_attribute_stats(
'schemaname', 'stats_import',
'relname', 'test_dom',
'attname', 'drange',
'inherited', false,
'range_length_histogram', '{2,4,4}'::text,
'range_empty_frac', '0'::real,
'range_bounds_histogram', '{"[1,3)","[5,9)","[11,15)"}'::text
);
SELECT *
FROM pg_stats
WHERE schemaname = 'stats_import'
AND tablename = 'test_dom'
AND inherited = false
AND attname = 'drange';
-- ok: range stats for a domain over a multirange type.
SELECT pg_catalog.pg_restore_attribute_stats(
'schemaname', 'stats_import',
'relname', 'test_dom',
'attname', 'dmrange',
'inherited', false,
'range_length_histogram', '{29,29,109}'::text,
'range_empty_frac', '0'::real,
'range_bounds_histogram', '{"[1,30)","[11,30)","[21,130)"}'::text
);
SELECT *
FROM pg_stats
WHERE schemaname = 'stats_import'
AND tablename = 'test_dom'
AND inherited = false
AND attname = 'dmrange';
-- warn: multirange values in the bounds histogram of a domain. These
-- must be ranges.
SELECT pg_catalog.pg_restore_attribute_stats(
'schemaname', 'stats_import',
'relname', 'test_dom',
'attname', 'dmrange',
'inherited', false,
'range_length_histogram', '{29,29,109}'::text,
'range_empty_frac', '0'::real,
'range_bounds_histogram', '{"{[1,30)}","{[11,30)}"}'::text
);
-- test for a domain over tsvector.
CREATE DOMAIN stats_import.dom_tsvector AS tsvector;
CREATE TABLE stats_import.test_dom_ts(
id int,
v stats_import.dom_tsvector
) WITH (autovacuum_enabled = false);
INSERT INTO stats_import.test_dom_ts
SELECT g, to_tsvector('english', 'the quick brown fox ' || g)
FROM generate_series(1, 20) AS g;
-- ok: mcelem and elem_count_histogram for a domain over tsvector
SELECT pg_catalog.pg_restore_attribute_stats(
'schemaname', 'stats_import',
'relname', 'test_dom_ts',
'attname', 'v',
'inherited', false,
'most_common_elems', '{brown,fox,quick}'::text,
'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[],
'elem_count_histogram', '{4,4,4,4,4,4,4,4,4,4}'::real[]
);
SELECT *
FROM pg_stats
WHERE schemaname = 'stats_import'
AND tablename = 'test_dom_ts'
AND inherited = false
AND attname = 'v';
--
-- Test the ability to exactly copy data from one table to an identical table,
-- correctly reconstructing the stakind order as well as the staopN and