From 753057e340da8f22e57c9a409ea7a235fd6a46bd Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Sat, 26 Sep 2026 09:28:18 +0900 Subject: [PATCH] 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 Author: Michael Paquier Reviewed-by: Jian He Reviewed-by: Corey Huinker Discussion: https://postgr.es/m/19715-b8be35083016f289@postgresql.org Backpatch-through: 18 --- src/backend/statistics/attribute_stats.c | 85 +++++++---- src/test/regress/expected/stats_import.out | 155 ++++++++++++++++++++- src/test/regress/sql/stats_import.sql | 113 +++++++++++++++ 3 files changed, 327 insertions(+), 26 deletions(-) diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c index 56293d94104..a99bcfedc38 100644 --- a/src/backend/statistics/attribute_stats.c +++ b/src/backend/statistics/attribute_stats.c @@ -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, <_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. * diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out index 78e5f4563fb..dc79e7425bc 100644 --- a/src/test/regress/expected/stats_import.out +++ b/src/test/regress/expected/stats_import.out @@ -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 diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql index 0ba5791e2e5..e68107628b7 100644 --- a/src/test/regress/sql/stats_import.sql +++ b/src/test/regress/sql/stats_import.sql @@ -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