From 8106950aceac31043fd13fb3c4d10da8cdaed863 Mon Sep 17 00:00:00 2001 From: Michael Paquier Date: Fri, 25 Sep 2026 08:34:31 +0900 Subject: [PATCH v2] 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. ANALYZE is able to generate such stats, incorporating the knowledge to handle domains in the in-core typanalyze callbacks. 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. 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 the typcache lookups), reflecting on the surrounding helper routines shared by both code paths. The fix for v18 is more local, 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 Discussion: https://postgr.es/m/19715-b8be35083016f289@postgresql.org Backpatch-through: 18 --- src/include/statistics/stat_utils.h | 8 +- src/backend/statistics/attribute_stats.c | 19 +- src/backend/statistics/extended_stats_funcs.c | 33 +- src/backend/statistics/stat_utils.c | 61 +++- src/test/regress/expected/stats_import.out | 332 +++++++++++++++++- src/test/regress/sql/stats_import.sql | 251 +++++++++++++ 6 files changed, 658 insertions(+), 46 deletions(-) diff --git a/src/include/statistics/stat_utils.h b/src/include/statistics/stat_utils.h index 15e962dbb7c8..aa034cc71971 100644 --- a/src/include/statistics/stat_utils.h +++ b/src/include/statistics/stat_utils.h @@ -18,6 +18,8 @@ /* avoid including primnodes.h here */ typedef struct RangeVar RangeVar; +/* avoid including typcache.h here */ +typedef struct TypeCacheEntry TypeCacheEntry; struct StatsArgInfo { @@ -43,7 +45,7 @@ extern bool stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo, extern void statatt_get_type(Oid reloid, AttrNumber attnum, Oid *atttypid, int32 *atttypmod, - char *atttyptype, Oid *atttypcoll, + TypeCacheEntry **basetypcache, Oid *atttypcoll, Oid *eq_opr, Oid *lt_opr); extern void statatt_init_empty_tuple(Oid reloid, int16 attnum, bool inherited, Datum *values, bool *nulls, bool *replaces); @@ -55,8 +57,10 @@ extern void statatt_set_slot(Datum *values, bool *nulls, bool *replaces, extern Datum statatt_build_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid, int32 typmod, bool *ok); -extern bool statatt_get_elem_type(Oid atttypid, char atttyptype, +extern bool statatt_get_elem_type(TypeCacheEntry *basetypcache, Oid *elemtypid, Oid *elem_eq_opr); +extern bool statatt_get_range_type(TypeCacheEntry *basetypcache, + Oid *rangetypid); extern bool statatt_check_bounds_histogram(Datum arrayval); diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c index c35892ce6d0b..25d8a73e5739 100644 --- a/src/backend/statistics/attribute_stats.c +++ b/src/backend/statistics/attribute_stats.c @@ -221,7 +221,7 @@ attribute_statistics_update_internal(Oid reloid, Oid atttypid = InvalidOid; int32 atttypmod; - char atttyptype; + TypeCacheEntry *basetypcache; Oid atttypcoll = InvalidOid; Oid eq_opr = InvalidOid; Oid lt_opr = InvalidOid; @@ -229,6 +229,8 @@ attribute_statistics_update_internal(Oid reloid, 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) && @@ -296,14 +298,13 @@ attribute_statistics_update_internal(Oid reloid, /* derive information from attribute */ statatt_get_type(reloid, attnum, &atttypid, &atttypmod, - &atttyptype, &atttypcoll, + &basetypcache, &atttypcoll, &eq_opr, <_opr); /* if needed, derive element type */ if (do_mcelem || do_dechist) { - if (!statatt_get_elem_type(atttypid, atttyptype, - &elemtypid, &elem_eq_opr)) + if (!statatt_get_elem_type(basetypcache, &elemtypid, &elem_eq_opr)) { ereport(WARNING, (errmsg("could not determine element type of column \"%s\"", attname), @@ -334,7 +335,7 @@ attribute_statistics_update_internal(Oid reloid, /* only range types can have range stats */ if ((do_range_length_histogram || do_bounds_histogram) && - !(atttyptype == TYPTYPE_RANGE || atttyptype == TYPTYPE_MULTIRANGE)) + !statatt_get_range_type(basetypcache, &bounds_typid)) { ereport(WARNING, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), @@ -498,14 +499,8 @@ attribute_statistics_update_internal(Oid reloid, { 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 = statatt_build_stavalues("range_bounds_histogram", &array_in_fn, diff --git a/src/backend/statistics/extended_stats_funcs.c b/src/backend/statistics/extended_stats_funcs.c index cb965fdb8680..00b8dbe33668 100644 --- a/src/backend/statistics/extended_stats_funcs.c +++ b/src/backend/statistics/extended_stats_funcs.c @@ -1115,7 +1115,7 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont, bool *pg_statistic_ok) { const char *argname = extarginfo[EXPRESSIONS_ARG].argname; - TypeCacheEntry *typcache; + TypeCacheEntry *basetypcache; Datum values[Natts_pg_statistic]; bool nulls[Natts_pg_statistic]; bool replaces[Natts_pg_statistic]; @@ -1123,6 +1123,7 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont, Datum pgstdat = (Datum) 0; Oid elemtypid = InvalidOid; Oid elemeqopr = InvalidOid; + Oid rtypid = InvalidOid; bool found[NUM_ATTRIBUTE_STATS_ELEMS] = {0}; JsonbValue val[NUM_ATTRIBUTE_STATS_ELEMS] = {0}; @@ -1221,7 +1222,13 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont, } /* This finds the right operators even if atttypid is a domain */ - typcache = lookup_type_cache(typid, TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR); + basetypcache = lookup_type_cache(typid, 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); statatt_init_empty_tuple(InvalidOid, InvalidAttrNumber, false, values, nulls, replaces); @@ -1230,7 +1237,7 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont, * Special case: collation for tsvector is DEFAULT_COLLATION_OID. See * compute_tsvector_stats(). */ - if (typid == TSVECTOROID) + if (basetypcache->type_id == TSVECTOROID) typcoll = DEFAULT_COLLATION_OID; /* @@ -1240,8 +1247,7 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont, */ if (found[MOST_COMMON_ELEMS_ELEM] || found[ELEM_COUNT_HISTOGRAM_ELEM]) { - if (!statatt_get_elem_type(typid, typcache->typtype, - &elemtypid, &elemeqopr)) + if (!statatt_get_elem_type(basetypcache, &elemtypid, &elemeqopr)) { ereport(WARNING, errcode(ERRCODE_INVALID_PARAMETER_VALUE), @@ -1259,8 +1265,7 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont, found[RANGE_EMPTY_FRAC_ELEM] || found[RANGE_BOUNDS_HISTOGRAM_ELEM]) { - if (typcache->typtype != TYPTYPE_RANGE && - typcache->typtype != TYPTYPE_MULTIRANGE) + if (!statatt_get_range_type(basetypcache, &rtypid)) { ereport(WARNING, errcode(ERRCODE_INVALID_PARAMETER_VALUE), @@ -1364,7 +1369,7 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont, statatt_set_slot(values, nulls, replaces, STATISTIC_KIND_MCV, - typcache->eq_opr, typcoll, + basetypcache->eq_opr, typcoll, stanumbers, false, stavalues, false); } else @@ -1386,7 +1391,7 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont, if (val_ok) statatt_set_slot(values, nulls, replaces, STATISTIC_KIND_HISTOGRAM, - typcache->lt_opr, typcoll, + basetypcache->lt_opr, typcoll, 0, true, stavalues, false); else goto pg_statistic_error; @@ -1405,7 +1410,7 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont, statatt_set_slot(values, nulls, replaces, STATISTIC_KIND_CORRELATION, - typcache->lt_opr, typcoll, + basetypcache->lt_opr, typcoll, stanumbers, false, 0, true); } else @@ -1476,14 +1481,8 @@ import_pg_statistic(Relation pgsd, JsonbContainer *cont, Datum stavalues; bool val_ok = false; char *s; - Oid rtypid = typid; - /* - * If it's a multirange, step down to the range type, as is done by - * multirange_typanalyze(). - */ - if (type_is_multirange(typid)) - rtypid = get_multirange_range(typid); + Assert(OidIsValid(rtypid)); s = jbv_string_get_cstr(&val[RANGE_BOUNDS_HISTOGRAM_ELEM]); diff --git a/src/backend/statistics/stat_utils.c b/src/backend/statistics/stat_utils.c index f4ff9ab9b20c..a36b2775595f 100644 --- a/src/backend/statistics/stat_utils.c +++ b/src/backend/statistics/stat_utils.c @@ -442,14 +442,13 @@ stats_fill_fcinfo_from_arg_pairs(FunctionCallInfo pairs_fcinfo, void statatt_get_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)); @@ -496,35 +495,42 @@ statatt_get_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); } /* - * Derive element type information from the attribute type. This information - * is needed when the given type is one that contains elements of other types. + * Derive element type information from the base type of an attribute. This + * information is needed when the given type is one that contains elements of + * other types. * - * The atttypid and atttyptype should be derived from a previous call to + * The type cache entry should be derived from a previous call to * statatt_get_type(). */ bool -statatt_get_elem_type(Oid atttypid, char atttyptype, +statatt_get_elem_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 @@ -534,8 +540,8 @@ statatt_get_elem_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)) @@ -551,6 +557,33 @@ statatt_get_elem_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 + * statatt_get_type(), so that any domain has already been looked through. + */ +bool +statatt_get_range_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; +} + /* * Build an array with element type typid from a text datum, used as * value of an attribute in a tuple to-be-inserted into pg_statistic. diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out index 4ce176c26678..8b54e2606260 100644 --- a/src/test/regress/expected/stats_import.out +++ b/src/test/regress/expected/stats_import.out @@ -1495,6 +1495,231 @@ 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 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 stats_import.pg_stats_stable +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 stats_import.pg_stats_stable +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 stats_import.pg_stats_stable +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) + +-- +-- Check that the range stats that ANALYZE generates for domains over range +-- and multirange types can be restored exactly. +-- +ANALYZE stats_import.test_dom; +CREATE TABLE stats_import.test_dom_clone ( LIKE stats_import.test_dom ) + WITH (autovacuum_enabled = false); +SELECT s.attname, s.inherited, r.* +FROM pg_catalog.pg_stats AS s +CROSS JOIN LATERAL + pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test_dom_clone', + 'attname', s.attname::text, + 'inherited', s.inherited, + 'null_frac', s.null_frac, + 'avg_width', s.avg_width, + 'n_distinct', s.n_distinct, + 'most_common_vals', s.most_common_vals::text, + 'most_common_freqs', s.most_common_freqs, + 'histogram_bounds', s.histogram_bounds::text, + 'correlation', s.correlation, + 'range_bounds_histogram', s.range_bounds_histogram::text, + 'range_empty_frac', s.range_empty_frac, + 'range_length_histogram', s.range_length_histogram::text) AS r +WHERE s.schemaname = 'stats_import' +AND s.tablename = 'test_dom' +ORDER BY s.attname, s.inherited; + attname | inherited | r +---------+-----------+--- + dmrange | f | t + drange | f | t + id | f | t +(3 rows) + +SELECT relname, (stats).* +FROM stats_import.pg_statistic_get_difference('test_dom', 'test_dom_clone') +\gx +(0 rows) + +-- 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 stats_import.pg_stats_stable +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) + +-- +-- Check that the statistics that ANALYZE generates for a domain over +-- tsvector can be restored exactly. +-- +ANALYZE stats_import.test_dom_ts; +CREATE TABLE stats_import.test_dom_ts_clone ( LIKE stats_import.test_dom_ts ) + WITH (autovacuum_enabled = false); +SELECT s.attname, s.inherited, r.* +FROM pg_catalog.pg_stats AS s +CROSS JOIN LATERAL + pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test_dom_ts_clone', + 'attname', s.attname::text, + 'inherited', s.inherited, + 'null_frac', s.null_frac, + 'avg_width', s.avg_width, + 'n_distinct', s.n_distinct, + 'most_common_vals', s.most_common_vals::text, + 'most_common_freqs', s.most_common_freqs, + 'histogram_bounds', s.histogram_bounds::text, + 'correlation', s.correlation, + 'most_common_elems', s.most_common_elems::text, + 'most_common_elem_freqs', s.most_common_elem_freqs, + 'elem_count_histogram', s.elem_count_histogram) AS r +WHERE s.schemaname = 'stats_import' +AND s.tablename = 'test_dom_ts' +ORDER BY s.attname, s.inherited; + attname | inherited | r +---------+-----------+--- + id | f | t + v | f | t +(2 rows) + +SELECT relname, (stats).* +FROM stats_import.pg_statistic_get_difference('test_dom_ts', 'test_dom_ts_clone') +\gx +(0 rows) + -- -- 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 @@ -2737,6 +2962,103 @@ range_length_histogram | {10179,10189,10199} range_empty_frac | 0 range_bounds_histogram | {"[1,10200)","[11,10200)","[21,10200)"} +-- Check import of range stats for expressions whose type is a domain over a +-- range or a multirange type. +CREATE STATISTICS stats_import.test_dom_stat + ON id, + (range_merge(drange, drange)::stats_import.dom_range), + ((dmrange + '{}'::int4multirange)::stats_import.dom_mrange) + FROM stats_import.test_dom; +-- warn: reject multirange values in the bounds histogram of a domain. These +-- must be ranges. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_dom', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_dom_stat', + 'inherited', false, + 'exprs', '[{"range_length_histogram": "{2,4,4}", + "range_empty_frac": "0", + "range_bounds_histogram": "{\"[1,3)\",\"[5,9)\",\"[11,15)\"}"}, + {"range_length_histogram": "{29,29,109}", + "range_empty_frac": "0", + "range_bounds_histogram": "{\"{[1,30)}\",\"{[11,30)}\"}"}]'::jsonb); +WARNING: malformed range literal: "{[1,30)}" +DETAIL: Missing left parenthesis or bracket. +HINT: Element "range_bounds_histogram" in expression -2 could not be parsed. + pg_restore_extended_stats +--------------------------- + f +(1 row) + +-- ok: range stats for domains over range and multirange types +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_dom', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_dom_stat', + 'inherited', false, + 'exprs', '[{"range_length_histogram": "{2,4,4}", + "range_empty_frac": "0", + "range_bounds_histogram": "{\"[1,3)\",\"[5,9)\",\"[11,15)\"}"}, + {"range_length_histogram": "{29,29,109}", + "range_empty_frac": "0", + "range_bounds_histogram": "{\"[1,30)\",\"[11,30)\",\"[21,130)\"}"}]'::jsonb); + pg_restore_extended_stats +--------------------------- + t +(1 row) + +SELECT e.expr, e.range_length_histogram, e.range_empty_frac, + e.range_bounds_histogram +FROM pg_stats_ext_exprs AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_dom_stat' AND + e.inherited = false +\gx +-[ RECORD 1 ]----------+-------------------------------------------------------------------------------- +expr | (range_merge((drange)::int4range, (drange)::int4range))::stats_import.dom_range +range_length_histogram | {2,4,4} +range_empty_frac | 0 +range_bounds_histogram | {"[1,3)","[5,9)","[11,15)"} +-[ RECORD 2 ]----------+-------------------------------------------------------------------------------- +expr | (((dmrange)::int4multirange + '{}'::int4multirange))::stats_import.dom_mrange +range_length_histogram | {29,29,109} +range_empty_frac | 0 +range_bounds_histogram | {"[1,30)","[11,30)","[21,130)"} + +-- Check import of MCELEM stats for an expression whose type is a domain +-- over tsvector. +CREATE STATISTICS stats_import.test_dom_ts_stat + ON id, (strip(v)::stats_import.dom_tsvector) + FROM stats_import.test_dom_ts; +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_dom_ts', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_dom_ts_stat', + 'inherited', false, + 'exprs', '[{"most_common_elems": "{brown,fox,quick}", + "most_common_elem_freqs": "{0.3,0.2,0.2,0.3,0.0}", + "elem_count_histogram": "{4,4,4,4,4,4,4,4,4,4}"}]'::jsonb); + pg_restore_extended_stats +--------------------------- + t +(1 row) + +SELECT e.expr, e.most_common_elems, e.most_common_elem_freqs, + e.elem_count_histogram +FROM pg_stats_ext_exprs AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_dom_ts_stat' AND + e.inherited = false +\gx +-[ RECORD 1 ]----------+-------------------------------------------------- +expr | (strip((v)::tsvector))::stats_import.dom_tsvector +most_common_elems | {brown,fox,quick} +most_common_elem_freqs | {0.3,0.2,0.2,0.3,0} +elem_count_histogram | {4,4,4,4,4,4,4,4,4,4} + -- Incorrect extended stats kind, exprs not supported SELECT pg_catalog.pg_restore_extended_stats( 'schemaname', 'stats_import', @@ -3828,7 +4150,7 @@ SELECT COUNT(*) FROM stats_import.test_range_expr_null (1 row) DROP SCHEMA stats_import CASCADE; -NOTICE: drop cascades to 19 other objects +NOTICE: drop cascades to 27 other objects DETAIL: drop cascades to view stats_import.pg_stats_stable drop cascades to view stats_import.pg_statistic_flat_t drop cascades to function stats_import.pg_statistic_flat(text) @@ -3845,6 +4167,14 @@ 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 table stats_import.test_dom_clone +drop cascades to type stats_import.dom_tsvector +drop cascades to table stats_import.test_dom_ts +drop cascades to table stats_import.test_dom_ts_clone drop cascades to table stats_import.test_clone drop cascades to table stats_import.test_mr_clone drop cascades to table stats_import.test_range_expr_null diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql index 748c9a2e0000..20ec479ae22b 100644 --- a/src/test/regress/sql/stats_import.sql +++ b/src/test/regress/sql/stats_import.sql @@ -1082,6 +1082,188 @@ 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 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 stats_import.pg_stats_stable +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 stats_import.pg_stats_stable +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 stats_import.pg_stats_stable +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 +); + +-- +-- Check that the range stats that ANALYZE generates for domains over range +-- and multirange types can be restored exactly. +-- +ANALYZE stats_import.test_dom; + +CREATE TABLE stats_import.test_dom_clone ( LIKE stats_import.test_dom ) + WITH (autovacuum_enabled = false); + +SELECT s.attname, s.inherited, r.* +FROM pg_catalog.pg_stats AS s +CROSS JOIN LATERAL + pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test_dom_clone', + 'attname', s.attname::text, + 'inherited', s.inherited, + 'null_frac', s.null_frac, + 'avg_width', s.avg_width, + 'n_distinct', s.n_distinct, + 'most_common_vals', s.most_common_vals::text, + 'most_common_freqs', s.most_common_freqs, + 'histogram_bounds', s.histogram_bounds::text, + 'correlation', s.correlation, + 'range_bounds_histogram', s.range_bounds_histogram::text, + 'range_empty_frac', s.range_empty_frac, + 'range_length_histogram', s.range_length_histogram::text) AS r +WHERE s.schemaname = 'stats_import' +AND s.tablename = 'test_dom' +ORDER BY s.attname, s.inherited; + +SELECT relname, (stats).* +FROM stats_import.pg_statistic_get_difference('test_dom', 'test_dom_clone') +\gx + +-- 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 stats_import.pg_stats_stable +WHERE schemaname = 'stats_import' +AND tablename = 'test_dom_ts' +AND inherited = false +AND attname = 'v'; + +-- +-- Check that the statistics that ANALYZE generates for a domain over +-- tsvector can be restored exactly. +-- +ANALYZE stats_import.test_dom_ts; + +CREATE TABLE stats_import.test_dom_ts_clone ( LIKE stats_import.test_dom_ts ) + WITH (autovacuum_enabled = false); + +SELECT s.attname, s.inherited, r.* +FROM pg_catalog.pg_stats AS s +CROSS JOIN LATERAL + pg_catalog.pg_restore_attribute_stats( + 'schemaname', 'stats_import', + 'relname', 'test_dom_ts_clone', + 'attname', s.attname::text, + 'inherited', s.inherited, + 'null_frac', s.null_frac, + 'avg_width', s.avg_width, + 'n_distinct', s.n_distinct, + 'most_common_vals', s.most_common_vals::text, + 'most_common_freqs', s.most_common_freqs, + 'histogram_bounds', s.histogram_bounds::text, + 'correlation', s.correlation, + 'most_common_elems', s.most_common_elems::text, + 'most_common_elem_freqs', s.most_common_elem_freqs, + 'elem_count_histogram', s.elem_count_histogram) AS r +WHERE s.schemaname = 'stats_import' +AND s.tablename = 'test_dom_ts' +ORDER BY s.attname, s.inherited; + +SELECT relname, (stats).* +FROM stats_import.pg_statistic_get_difference('test_dom_ts', 'test_dom_ts_clone') +\gx + -- -- 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 @@ -1934,6 +2116,75 @@ WHERE e.statistics_schemaname = 'stats_import' AND e.inherited = false \gx +-- Check import of range stats for expressions whose type is a domain over a +-- range or a multirange type. +CREATE STATISTICS stats_import.test_dom_stat + ON id, + (range_merge(drange, drange)::stats_import.dom_range), + ((dmrange + '{}'::int4multirange)::stats_import.dom_mrange) + FROM stats_import.test_dom; + +-- warn: reject multirange values in the bounds histogram of a domain. These +-- must be ranges. +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_dom', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_dom_stat', + 'inherited', false, + 'exprs', '[{"range_length_histogram": "{2,4,4}", + "range_empty_frac": "0", + "range_bounds_histogram": "{\"[1,3)\",\"[5,9)\",\"[11,15)\"}"}, + {"range_length_histogram": "{29,29,109}", + "range_empty_frac": "0", + "range_bounds_histogram": "{\"{[1,30)}\",\"{[11,30)}\"}"}]'::jsonb); + +-- ok: range stats for domains over range and multirange types +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_dom', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_dom_stat', + 'inherited', false, + 'exprs', '[{"range_length_histogram": "{2,4,4}", + "range_empty_frac": "0", + "range_bounds_histogram": "{\"[1,3)\",\"[5,9)\",\"[11,15)\"}"}, + {"range_length_histogram": "{29,29,109}", + "range_empty_frac": "0", + "range_bounds_histogram": "{\"[1,30)\",\"[11,30)\",\"[21,130)\"}"}]'::jsonb); + +SELECT e.expr, e.range_length_histogram, e.range_empty_frac, + e.range_bounds_histogram +FROM pg_stats_ext_exprs AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_dom_stat' AND + e.inherited = false +\gx + +-- Check import of MCELEM stats for an expression whose type is a domain +-- over tsvector. +CREATE STATISTICS stats_import.test_dom_ts_stat + ON id, (strip(v)::stats_import.dom_tsvector) + FROM stats_import.test_dom_ts; + +SELECT pg_catalog.pg_restore_extended_stats( + 'schemaname', 'stats_import', + 'relname', 'test_dom_ts', + 'statistics_schemaname', 'stats_import', + 'statistics_name', 'test_dom_ts_stat', + 'inherited', false, + 'exprs', '[{"most_common_elems": "{brown,fox,quick}", + "most_common_elem_freqs": "{0.3,0.2,0.2,0.3,0.0}", + "elem_count_histogram": "{4,4,4,4,4,4,4,4,4,4}"}]'::jsonb); + +SELECT e.expr, e.most_common_elems, e.most_common_elem_freqs, + e.elem_count_histogram +FROM pg_stats_ext_exprs AS e +WHERE e.statistics_schemaname = 'stats_import' AND + e.statistics_name = 'test_dom_ts_stat' AND + e.inherited = false +\gx + -- Incorrect extended stats kind, exprs not supported SELECT pg_catalog.pg_restore_extended_stats( 'schemaname', 'stats_import', -- 2.55.0