v27j-0002-Create-function-pg_set_attribute_stats.patch
text/x-patch
Filename: v27j-0002-Create-function-pg_set_attribute_stats.patch
Type: text/x-patch
Part: 1
Message:
Re: Statistics Import and Export
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: format-patch
Series: patch 0002
Subject: Create function pg_set_attribute_stats.
| File | + | − |
|---|---|---|
| doc/src/sgml/func.sgml | 60 | 0 |
| src/backend/catalog/system_functions.sql | 22 | 0 |
| src/backend/statistics/import_stats.c | 827 | 0 |
| src/include/catalog/pg_proc.dat | 7 | 0 |
| src/include/statistics/statistics.h | 3 | 0 |
| src/test/regress/expected/stats_import.out | 644 | 1 |
| src/test/regress/sql/stats_import.sql | 544 | 0 |
From abd713bac2f25a6ab743fba1c6a486e21fb06cad Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@j-davis.com>
Date: Thu, 8 Aug 2024 17:56:22 -0700
Subject: [PATCH v27j 2/2] Create function pg_set_attribute_stats.
---
doc/src/sgml/func.sgml | 60 ++
src/backend/catalog/system_functions.sql | 22 +
src/backend/statistics/import_stats.c | 827 +++++++++++++++++++++
src/include/catalog/pg_proc.dat | 7 +
src/include/statistics/statistics.h | 3 +
src/test/regress/expected/stats_import.out | 645 +++++++++++++++-
src/test/regress/sql/stats_import.sql | 544 ++++++++++++++
7 files changed, 2107 insertions(+), 1 deletion(-)
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 02d93f6c925..402a14fdb1f 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29999,6 +29999,66 @@ DETAIL: Make sure pg_wal_replay_wait() isn't called within a transaction with a
</warning>
</entry>
</row>
+ <row>
+ <entry role="func_table_entry">
+ <para role="func_signature">
+ <indexterm>
+ <primary>pg_set_attribute_stats</primary>
+ </indexterm>
+ <function>pg_set_attribute_stats</function> (
+ <parameter>relation</parameter> <type>regclass</type>,
+ <parameter>attname</parameter> <type>name</type>,
+ <parameter>inherited</parameter> <type>boolean</type>,
+ <parameter>null_frac</parameter> <type>real</type>,
+ <parameter>avg_width</parameter> <type>integer</type>,
+ <parameter>n_distinct</parameter> <type>real</type>
+ <optional>, <parameter>most_common_vals</parameter> <type>text</type>, <parameter>most_common_freqs</parameter> <type>real[]</type> </optional>
+ <optional>, <parameter>histogram_bounds</parameter> <type>text</type> </optional>
+ <optional>, <parameter>correlation</parameter> <type>real</type> </optional>
+ <optional>, <parameter>most_common_elems</parameter> <type>text</type>, <parameter>most_common_elem_freqs</parameter> <type>real[]</type> </optional>
+ <optional>, <parameter>elem_count_histogram</parameter> <type>real[]</type> </optional>
+ <optional>, <parameter>range_length_histogram</parameter> <type>text</type> </optional>
+ <optional>, <parameter>range_empty_frac</parameter> <type>real</type> </optional>
+ <optional>, <parameter>range_bounds_histogram</parameter> <type>text</type> </optional> )
+ <returnvalue>void</returnvalue>
+ </para>
+ <para>
+ Updates attribute-level statistics for the given relation and
+ attribute name to the specified values. The parameters correspond to
+ to attributes of the same name found in the <link
+ linkend="view-pg-stats"><structname>pg_stats</structname></link>
+ view, and the values supplied in the parameter must meet the
+ requirements of the corresponding attribute.
+ </para>
+ <para>
+ Any optional parameters left unspecified will cause the corresponding
+ statistics to be set to <literal>NULL</literal>.
+ </para>
+ <para>
+ Ordinarily, these statistics are collected automatically or updated
+ as a part of <xref linkend="sql-vacuum"/> or <xref
+ linkend="sql-analyze"/>, so it's not necessary to call this
+ function. However, it may be useful when testing the effects of
+ statistics on the planner to understand or anticipate plan changes.
+ </para>
+ <para>
+ The caller must have the <literal>MAINTAIN</literal> privilege on
+ the table or be the owner of the database.
+ </para>
+ <warning>
+ <para>
+ Changes made by this function are likely to be overwritten by <link
+ linkend="autovacuum">autovacuum</link> (or manual
+ <command>VACUUM</command> or <command>ANALYZE</command>) and should
+ be considered temporary.
+ </para>
+ <para>
+ The signature of this function may change in new major releases if
+ there are changes to the statistics being tracked.
+ </para>
+ </warning>
+ </entry>
+ </row>
</tbody>
</tgroup>
</table>
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 623b9539b15..49f0d0dab2d 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -639,6 +639,28 @@ LANGUAGE INTERNAL
CALLED ON NULL INPUT VOLATILE PARALLEL SAFE
AS 'pg_stat_reset_slru';
+CREATE OR REPLACE FUNCTION
+ pg_set_attribute_stats(relation oid,
+ attname name,
+ inherited bool,
+ null_frac real,
+ avg_width integer,
+ n_distinct real,
+ most_common_vals text DEFAULT NULL,
+ most_common_freqs real[] DEFAULT NULL,
+ histogram_bounds text DEFAULT NULL,
+ correlation real DEFAULT NULL,
+ most_common_elems text DEFAULT NULL,
+ most_common_elem_freqs real[] DEFAULT NULL,
+ elem_count_histogram real[] DEFAULT NULL,
+ range_length_histogram text DEFAULT NULL,
+ range_empty_frac real DEFAULT NULL,
+ range_bounds_histogram text DEFAULT NULL)
+RETURNS void
+LANGUAGE INTERNAL
+CALLED ON NULL INPUT VOLATILE
+AS 'pg_set_attribute_stats';
+
--
-- The default permissions for functions mean that anyone can execute them.
-- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/statistics/import_stats.c b/src/backend/statistics/import_stats.c
index fede53f6634..7a2e26aae01 100644
--- a/src/backend/statistics/import_stats.c
+++ b/src/backend/statistics/import_stats.c
@@ -19,16 +19,55 @@
#include "access/heapam.h"
#include "catalog/indexing.h"
+#include "catalog/pg_collation.h"
#include "catalog/pg_database.h"
+#include "catalog/pg_operator.h"
#include "miscadmin.h"
+#include "nodes/nodeFuncs.h"
+#include "statistics/statistics.h"
#include "utils/acl.h"
+#include "utils/array.h"
#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
#include "utils/syscache.h"
static bool relation_statistics_update(Oid reloid, int version,
int32 relpages, float4 reltuples,
int32 relallvisible, int elevel);
+static bool attribute_statistics_update(Oid reloid, AttrNumber attnum, int version,
+ int elevel, bool inherited, float null_frac,
+ int avg_width, float n_distinct,
+ Datum mc_vals_datum, bool mc_vals_isnull,
+ Datum mc_freqs_datum, bool mc_freqs_isnull,
+ Datum histogram_bounds_datum, bool histogram_bounds_isnull,
+ Datum correlation_datum, bool correlation_isnull,
+ Datum mc_elems_datum, bool mc_elems_isnull,
+ Datum mc_elem_freqs_datum, bool mc_elem_freqs_isnull,
+ Datum elem_count_hist_datum, bool elem_count_hist_isnull,
+ Datum range_length_hist_datum, bool range_length_hist_isnull,
+ Datum range_empty_frac_datum, bool range_empty_frac_isnull,
+ Datum range_bounds_hist_datum, bool range_bounds_hist_isnull);
+static Node * get_attr_expr(Relation rel, int attnum);
+static void get_attr_stat_type(Relation rel, AttrNumber attnum, int elevel,
+ Oid *atttypid, int32 *atttypmod,
+ char *atttyptype, Oid *atttypcoll,
+ Oid *eq_opr, Oid *lt_opr);
+static bool get_elem_stat_type(Oid atttypid, char atttyptype, int elevel,
+ Oid *elemtypid, Oid *elem_eq_opr);
+static Datum text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d,
+ Oid typid, int32 typmod, int elevel, bool *ok);
+static void use_stats_slot(Datum *values, bool *nulls, int slotidx,
+ int16 stakind, Oid staop, Oid stacoll,
+ Datum stanumbers, bool stanumbers_isnull,
+ Datum stavalues, bool stavalues_isnull);
+static void update_pg_statistic(Datum values[], bool nulls[]);
static void check_privileges(Relation rel);
+static void check_arg_array(const char *staname, Datum arr, bool *isnull,
+ int elevel);
+static void check_arg_pair(const char *arg1name, bool *arg1null,
+ const char *arg2name, bool *arg2null,
+ int elevel);
/*
* Internal function for modifying statistics for a relation.
@@ -130,6 +169,617 @@ relation_statistics_update(Oid reloid, int version, int32 relpages,
PG_RETURN_BOOL(true);
}
+/*
+ * Insert or Update Attribute Statistics
+ *
+ * Major errors, such as the table not existing, the attribute not existing,
+ * or a permissions failure are always reported at ERROR. Other errors, such
+ * as a conversion failure, are reported at 'elevel', and a partial update
+ * will result.
+ *
+ * See pg_statistic.h for an explanation of how each statistic kind is
+ * stored. Custom statistics kinds are not supported.
+ *
+ * Depending on the statistics kind, we need to derive information from the
+ * attribute for which we're storing the stats. For instance, the MCVs are
+ * stored as an anyarray, and the representation of the array needs to store
+ * the correct element type, which must be derived from the attribute.
+ */
+static bool
+attribute_statistics_update(Oid reloid, AttrNumber attnum, int version,
+ int elevel, bool inherited, float null_frac,
+ int avg_width, float n_distinct,
+ Datum mc_vals_datum, bool mc_vals_isnull,
+ Datum mc_freqs_datum, bool mc_freqs_isnull,
+ Datum histogram_bounds_datum, bool histogram_bounds_isnull,
+ Datum correlation_datum, bool correlation_isnull,
+ Datum mc_elems_datum, bool mc_elems_isnull,
+ Datum mc_elem_freqs_datum, bool mc_elem_freqs_isnull,
+ Datum elem_count_hist_datum, bool elem_count_hist_isnull,
+ Datum range_length_hist_datum, bool range_length_hist_isnull,
+ Datum range_empty_frac_datum, bool range_empty_frac_isnull,
+ Datum range_bounds_hist_datum, bool range_bounds_hist_isnull)
+{
+ Relation rel;
+
+ Oid atttypid;
+ int32 atttypmod;
+ char atttyptype;
+ Oid atttypcoll;
+ Oid eq_opr;
+ Oid lt_opr;
+
+ Oid elemtypid;
+ Oid elem_eq_opr;
+
+ FmgrInfo array_in_fn;
+
+ Datum values[Natts_pg_statistic];
+ bool nulls[Natts_pg_statistic];
+
+ int slotidx = 0; /* slot in pg_statistic (1..5), minus one */
+ char *attname = get_attname(reloid, attnum, false);
+
+ /*
+ * Initialize nulls array to be false for all non-NULL attributes, and
+ * true for all nullable attributes.
+ */
+ for (int i = 0; i < Natts_pg_statistic; i++)
+ {
+ values[i] = (Datum) 0;
+ if (i < Anum_pg_statistic_stanumbers1 - 1)
+ nulls[i] = false;
+ else
+ nulls[i] = true;
+ }
+
+ check_arg_array("most_common_freqs", mc_freqs_datum,
+ &mc_freqs_isnull, elevel);
+ check_arg_array("most_common_elem_freqs", mc_elem_freqs_datum,
+ &mc_elem_freqs_isnull, elevel);
+ check_arg_array("elem_count_histogram", elem_count_hist_datum,
+ &elem_count_hist_isnull, elevel);
+
+ /* STATISTIC_KIND_MCV */
+ check_arg_pair("most_common_vals", &mc_vals_isnull,
+ "most_common_freqs", &mc_freqs_isnull,
+ elevel);
+
+ /* STATISTIC_KIND_MCELEM */
+ check_arg_pair("most_common_elems", &mc_elems_isnull,
+ "most_common_freqs", &mc_elem_freqs_isnull,
+ elevel);
+
+ /* STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM */
+ check_arg_pair("range_length_histogram", &range_length_hist_isnull,
+ "range_empty_frac", &range_empty_frac_isnull,
+ elevel);
+
+ rel = relation_open(reloid, ShareUpdateExclusiveLock);
+
+ check_privileges(rel);
+
+ /* derive information from attribute */
+ get_attr_stat_type(rel, attnum, elevel,
+ &atttypid, &atttypmod,
+ &atttyptype, &atttypcoll,
+ &eq_opr, <_opr);
+
+ /* if needed, derive element type */
+ if (!mc_elems_isnull || !elem_count_hist_isnull)
+ {
+ if (!get_elem_stat_type(atttypid, atttyptype, elevel,
+ &elemtypid, &elem_eq_opr))
+ {
+ ereport(elevel,
+ (errmsg("unable to determine element type of attribute \"%s\"", attname),
+ errdetail("Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.")));
+ elemtypid = InvalidOid;
+ elem_eq_opr = InvalidOid;
+ mc_elems_isnull = true;
+ elem_count_hist_isnull = true;
+ }
+ }
+
+ /* histogram and correlation require less-than operator */
+ if ((!histogram_bounds_isnull || !correlation_isnull) &&
+ !OidIsValid(lt_opr))
+ {
+ ereport(elevel,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("could not determine less-than operator for attribute \"%s\"", attname),
+ errdetail("Cannot set STATISTIC_KIND_HISTOGRAM or STATISTIC_KIND_CORRELATION.")));
+ histogram_bounds_isnull = true;
+ correlation_isnull = true;
+ }
+
+ /* only range types can have range stats */
+ if ((!range_length_hist_isnull || !range_bounds_hist_isnull) &&
+ !(atttyptype == TYPTYPE_RANGE || atttyptype == TYPTYPE_MULTIRANGE))
+ {
+ ereport(elevel,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("attribute \"%s\" is not a range type", attname),
+ errdetail("Cannot set STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM or STATISTIC_KIND_BOUNDS_HISTOGRAM.")));
+
+ range_length_hist_isnull = true;
+ range_empty_frac_isnull = true;
+ }
+
+ fmgr_info(F_ARRAY_IN, &array_in_fn);
+
+ /* Populate pg_statistic tuple */
+ values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(reloid);
+ values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attnum);
+ values[Anum_pg_statistic_stainherit - 1] = BoolGetDatum(inherited);
+ values[Anum_pg_statistic_stanullfrac - 1] = Float4GetDatum(null_frac);
+ values[Anum_pg_statistic_stawidth - 1] = Int32GetDatum(avg_width);
+ values[Anum_pg_statistic_stadistinct - 1] = Float4GetDatum(n_distinct);
+
+ /*
+ * STATISTIC_KIND_MCV
+ *
+ * Convert most_common_vals from text to anyarray, where the element type
+ * is the attribute type, and store in stavalues. Store most_common_freqs
+ * in stanumbers.
+ */
+ if (!mc_vals_isnull)
+ {
+ bool converted;
+ Datum stanumbers = mc_freqs_datum;
+ Datum stavalues = text_to_stavalues("most_common_vals",
+ &array_in_fn, mc_vals_datum,
+ atttypid, atttypmod,
+ elevel, &converted);
+
+ if (converted)
+ {
+ use_stats_slot(values, nulls, slotidx++,
+ STATISTIC_KIND_MCV,
+ eq_opr, atttypcoll,
+ stanumbers, false, stavalues, false);
+ }
+ else
+ {
+ mc_vals_isnull = true;
+ mc_freqs_isnull = true;
+ }
+ }
+
+ /*
+ * STATISTIC_KIND_HISTOGRAM
+ *
+ * histogram_bounds: ANYARRAY::text
+ */
+ if (!histogram_bounds_isnull)
+ {
+ Datum stavalues;
+ bool converted = false;
+
+ stavalues = text_to_stavalues("histogram_bounds",
+ &array_in_fn, histogram_bounds_datum,
+ atttypid, atttypmod, elevel,
+ &converted);
+
+ if (converted)
+ {
+ use_stats_slot(values, nulls, slotidx++,
+ STATISTIC_KIND_HISTOGRAM,
+ lt_opr, atttypcoll,
+ 0, true, stavalues, false);
+ }
+ else
+ histogram_bounds_isnull = true;
+ }
+
+ /*
+ * STATISTIC_KIND_CORRELATION
+ *
+ * correlation: real
+ */
+ if (!correlation_isnull)
+ {
+ Datum elems[] = {correlation_datum};
+ ArrayType *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+ Datum stanumbers = PointerGetDatum(arry);
+
+ use_stats_slot(values, nulls, slotidx++,
+ STATISTIC_KIND_CORRELATION,
+ lt_opr, atttypcoll,
+ stanumbers, false, 0, true);
+ }
+
+ /*
+ * STATISTIC_KIND_MCELEM
+ *
+ * most_common_elem_freqs: real[]
+ *
+ * most_common_elems : ANYARRAY::text
+ */
+ if (!mc_elems_isnull)
+ {
+ Datum stanumbers = mc_elem_freqs_datum;
+ bool converted = false;
+ Datum stavalues;
+
+ stavalues = text_to_stavalues("most_common_elems",
+ &array_in_fn, mc_elems_datum,
+ elemtypid, atttypmod,
+ elevel, &converted);
+
+ if (converted)
+ {
+ use_stats_slot(values, nulls, slotidx++,
+ STATISTIC_KIND_MCELEM,
+ elem_eq_opr, atttypcoll,
+ stanumbers, false, stavalues, false);
+ }
+ else
+ {
+ /* the mc_elem stat did not write */
+ mc_elems_isnull = true;
+ mc_elem_freqs_isnull = true;
+ }
+ }
+
+ /*
+ * STATISTIC_KIND_DECHIST
+ *
+ * elem_count_histogram: real[]
+ */
+ if (!elem_count_hist_isnull)
+ {
+ Datum stanumbers = elem_count_hist_datum;
+
+ use_stats_slot(values, nulls, slotidx++,
+ STATISTIC_KIND_DECHIST,
+ elem_eq_opr, atttypcoll,
+ stanumbers, false, 0, true);
+ }
+
+ /*
+ * STATISTIC_KIND_BOUNDS_HISTOGRAM
+ *
+ * range_bounds_histogram: ANYARRAY::text
+ *
+ * This stakind appears before STATISTIC_KIND_BOUNDS_HISTOGRAM even though
+ * it is numerically greater, and all other stakinds appear in numerical
+ * order. We duplicate this quirk to make before/after tests of
+ * pg_statistic records easier.
+ */
+ if (!range_bounds_hist_isnull)
+ {
+ bool converted = false;
+ Datum stavalues;
+
+ stavalues = text_to_stavalues("range_bounds_histogram",
+ &array_in_fn, range_bounds_hist_datum,
+ atttypid, atttypmod,
+ elevel, &converted);
+
+ if (converted)
+ {
+ use_stats_slot(values, nulls, slotidx++,
+ STATISTIC_KIND_BOUNDS_HISTOGRAM,
+ InvalidOid, InvalidOid,
+ 0, true, stavalues, false);
+ }
+ else
+ range_bounds_hist_isnull = true;
+ }
+
+ /*
+ * STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM
+ *
+ * range_empty_frac: real
+ *
+ * range_length_histogram: double precision[]::text
+ */
+ if (!range_length_hist_isnull)
+ {
+ /* The anyarray is always a float8[] for this stakind */
+ Datum elems[] = {range_empty_frac_datum};
+ ArrayType *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+ Datum stanumbers = PointerGetDatum(arry);
+
+ bool converted = false;
+ Datum stavalues;
+
+ stavalues = text_to_stavalues("range_length_histogram",
+ &array_in_fn, range_length_hist_datum,
+ FLOAT8OID, 0, elevel, &converted);
+
+ if (converted)
+ {
+ use_stats_slot(values, nulls, slotidx++,
+ STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM,
+ Float8LessOperator, InvalidOid,
+ stanumbers, false, stavalues, false);
+ }
+ else
+ {
+ range_empty_frac_isnull = true;
+ range_length_hist_isnull = true;
+ }
+ }
+
+ update_pg_statistic(values, nulls);
+
+ relation_close(rel, NoLock);
+
+ return true;
+}
+
+/*
+ * If this relation is an index and that index has expressions in it, and
+ * the attnum specified is known to be an expression, then we must walk
+ * the list attributes up to the specified attnum to get the right
+ * expression.
+ */
+static Node *
+get_attr_expr(Relation rel, int attnum)
+{
+ if ((rel->rd_rel->relkind == RELKIND_INDEX
+ || (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+ && (rel->rd_indexprs != NIL)
+ && (rel->rd_index->indkey.values[attnum - 1] == 0))
+ {
+ ListCell *indexpr_item = list_head(rel->rd_indexprs);
+
+ for (int i = 0; i < attnum - 1; i++)
+ if (rel->rd_index->indkey.values[i] == 0)
+ indexpr_item = lnext(rel->rd_indexprs, indexpr_item);
+
+ if (indexpr_item == NULL) /* shouldn't happen */
+ elog(ERROR, "too few entries in indexprs list");
+
+ return (Node *) lfirst(indexpr_item);
+ }
+ return NULL;
+}
+
+/*
+ * Derive type information from the attribute.
+ */
+static void
+get_attr_stat_type(Relation rel, AttrNumber attnum, int elevel,
+ Oid *atttypid, int32 *atttypmod,
+ char *atttyptype, Oid *atttypcoll,
+ Oid *eq_opr, Oid *lt_opr)
+{
+ Oid relid = RelationGetRelid(rel);
+ Form_pg_attribute attr;
+ HeapTuple atup;
+ Node *expr;
+ TypeCacheEntry *typcache;
+
+ atup = SearchSysCache2(ATTNUM, ObjectIdGetDatum(relid),
+ Int16GetDatum(attnum));
+
+ /* Attribute not found */
+ if (!HeapTupleIsValid(atup))
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("attribute %d of relation with OID %u does not exist",
+ attnum, relid)));
+
+ attr = (Form_pg_attribute) GETSTRUCT(atup);
+
+ if (attr->attisdropped)
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_COLUMN),
+ errmsg("attribute %d of relation with OID %u does not exist",
+ attnum, RelationGetRelid(rel))));
+
+ expr = get_attr_expr(rel, attr->attnum);
+
+ if (expr == NULL)
+ {
+ *atttypid = attr->atttypid;
+ *atttypmod = attr->atttypmod;
+ *atttypcoll = attr->attcollation;
+ }
+ else
+ {
+ *atttypid = exprType(expr);
+ *atttypmod = exprTypmod(expr);
+
+ /* TODO: better explanation */
+ /*
+ * If a collation has been specified for the index column, use that in
+ * preference to anything else; but if not, fall back to whatever we
+ * can get from the expression.
+ */
+ if (OidIsValid(attr->attcollation))
+ *atttypcoll = attr->attcollation;
+ else
+ *atttypcoll = exprCollation(expr);
+ }
+ ReleaseSysCache(atup);
+
+ /* TODO: better explanation */
+ /* if it's a multirange, step down to the range type */
+ if (type_is_multirange(*atttypid))
+ *atttypid = get_multirange_range(*atttypid);
+
+ typcache = lookup_type_cache(*atttypid, TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR);
+ *atttyptype = typcache->typtype;
+ *eq_opr = typcache->eq_opr;
+ *lt_opr = typcache->lt_opr;
+
+ /* TODO: explain special case for tsvector */
+ if (*atttypid == TSVECTOROID)
+ *atttypcoll = DEFAULT_COLLATION_OID;
+}
+
+/*
+ * Derive element type information from the attribute type.
+ */
+static bool
+get_elem_stat_type(Oid atttypid, char atttyptype, int elevel,
+ Oid *elemtypid, Oid *elem_eq_opr)
+{
+ TypeCacheEntry *elemtypcache;
+
+ /* TODO: explain special case for tsvector */
+ if (atttypid == TSVECTOROID)
+ *elemtypid = TEXTOID;
+ else if (atttyptype == TYPTYPE_RANGE)
+ *elemtypid = get_range_subtype(atttypid);
+ else
+ *elemtypid = get_base_element_type(atttypid);
+
+ if (!OidIsValid(*elemtypid))
+ return false;
+
+ elemtypcache = lookup_type_cache(*elemtypid, TYPECACHE_EQ_OPR);
+ if (!OidIsValid(elemtypcache->eq_opr))
+ return false;
+
+ *elem_eq_opr = elemtypcache->eq_opr;
+
+ return true;
+}
+
+/*
+ * Cast a text datum into an array with element type elemtypid.
+ *
+ * If an error is encountered, capture it and re-throw at elevel, and set ok
+ * to false. If the resulting array contains NULLs, raise an error at elevel
+ * and set ok to false. Otherwise, set ok to true.
+ */
+static Datum
+text_to_stavalues(const char *staname, FmgrInfo *array_in, Datum d, Oid typid,
+ int32 typmod, int elevel, bool *ok)
+{
+ LOCAL_FCINFO(fcinfo, 8);
+ char *s;
+ Datum result;
+ ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+ escontext.details_wanted = true;
+
+ s = TextDatumGetCString(d);
+
+ InitFunctionCallInfoData(*fcinfo, array_in, 3, InvalidOid,
+ (Node *) &escontext, NULL);
+
+ fcinfo->args[0].value = CStringGetDatum(s);
+ fcinfo->args[0].isnull = false;
+ fcinfo->args[1].value = ObjectIdGetDatum(typid);
+ fcinfo->args[1].isnull = false;
+ fcinfo->args[2].value = Int32GetDatum(typmod);
+ fcinfo->args[2].isnull = false;
+
+ result = FunctionCallInvoke(fcinfo);
+
+ pfree(s);
+
+ if (SOFT_ERROR_OCCURRED(&escontext))
+ {
+ if (elevel != ERROR)
+ escontext.error_data->elevel = elevel;
+ ThrowErrorData(escontext.error_data);
+ *ok = false;
+ return (Datum)0;
+ }
+
+ if (array_contains_nulls(DatumGetArrayTypeP(result)))
+ {
+ ereport(elevel,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("\"%s\" array cannot contain NULL values", staname)));
+ *ok = false;
+ return (Datum)0;
+ }
+
+ *ok = true;
+
+ return result;
+}
+
+static void
+use_stats_slot(Datum *values, bool *nulls, int slotidx,
+ int16 stakind, Oid staop, Oid stacoll,
+ Datum stanumbers, bool stanumbers_isnull,
+ Datum stavalues, bool stavalues_isnull)
+{
+ if (slotidx >= STATISTIC_NUM_SLOTS)
+ ereport(ERROR,
+ (errmsg("maximum number of statistics slots exceeded: %d", slotidx + 1)));
+
+ /* slot should not be taken */
+ Assert(values[Anum_pg_statistic_stakind1 - 1 + slotidx] == (Datum) 0);
+ Assert(values[Anum_pg_statistic_staop1 - 1 + slotidx] == (Datum) 0);
+ Assert(values[Anum_pg_statistic_stacoll1 - 1 + slotidx] == (Datum) 0);
+ Assert(values[Anum_pg_statistic_stanumbers1 - 1 + slotidx] == (Datum) 0);
+ Assert(values[Anum_pg_statistic_stavalues1 - 1 + slotidx] == (Datum) 0);
+
+ /* nulls should be false for non-NULL attributes, true for nullable */
+ Assert(!nulls[Anum_pg_statistic_stakind1 - 1 + slotidx]);
+ Assert(!nulls[Anum_pg_statistic_staop1 - 1 + slotidx]);
+ Assert(!nulls[Anum_pg_statistic_stacoll1 - 1 + slotidx]);
+ Assert(nulls[Anum_pg_statistic_stanumbers1 - 1 + slotidx]);
+ Assert(nulls[Anum_pg_statistic_stavalues1 - 1 + slotidx]);
+
+ values[Anum_pg_statistic_stakind1 - 1 + slotidx] = stakind;
+ values[Anum_pg_statistic_staop1 - 1 + slotidx] = staop;
+ values[Anum_pg_statistic_stacoll1 - 1 + slotidx] = stacoll;
+
+ if (!stanumbers_isnull)
+ {
+ values[Anum_pg_statistic_stanumbers1 - 1 + slotidx] = stanumbers;
+ nulls[Anum_pg_statistic_stanumbers1 - 1 + slotidx] = false;
+ }
+ if (!stavalues_isnull)
+ {
+ values[Anum_pg_statistic_stavalues1 - 1 + slotidx] = stavalues;
+ nulls[Anum_pg_statistic_stavalues1 - 1 + slotidx] = false;
+ }
+}
+
+/*
+ * Update the pg_statistic record.
+ */
+static void
+update_pg_statistic(Datum values[], bool nulls[])
+{
+ Relation sd = table_open(StatisticRelationId, RowExclusiveLock);
+ CatalogIndexState indstate = CatalogOpenIndexes(sd);
+ HeapTuple oldtup;
+ HeapTuple stup;
+
+ /* Is there already a pg_statistic tuple for this attribute? */
+ oldtup = SearchSysCache3(STATRELATTINH,
+ values[Anum_pg_statistic_starelid - 1],
+ values[Anum_pg_statistic_staattnum - 1],
+ values[Anum_pg_statistic_stainherit - 1]);
+
+ if (HeapTupleIsValid(oldtup))
+ {
+ /* Yes, replace it */
+ bool replaces[Natts_pg_statistic];
+
+ for (int i = 0; i < Natts_pg_statistic; i++)
+ replaces[i] = true;
+
+ stup = heap_modify_tuple(oldtup, RelationGetDescr(sd),
+ values, nulls, replaces);
+ ReleaseSysCache(oldtup);
+ CatalogTupleUpdateWithInfo(sd, &stup->t_self, stup, indstate);
+ }
+ else
+ {
+ /* No, insert new tuple */
+ stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+ CatalogTupleInsertWithInfo(sd, stup, indstate);
+ }
+
+ heap_freetuple(stup);
+ CatalogCloseIndexes(indstate);
+ table_close(sd, RowExclusiveLock);
+}
+
/*
* A role has privileges to set statistics on the relation if any of the
* following are true:
@@ -153,6 +803,65 @@ check_privileges(Relation rel)
NameStr(rel->rd_rel->relname));
}
+/*
+ * Check that array argument is one dimensional with no NULLs.
+ */
+static void
+check_arg_array(const char *staname, Datum datum, bool *isnull, int elevel)
+{
+ ArrayType *arr;
+
+ if (*isnull)
+ return;
+
+ arr = DatumGetArrayTypeP(datum);
+
+ if (ARR_NDIM(arr) != 1)
+ {
+ ereport(elevel,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("\"%s\" cannot be a multidimensional array", staname)));
+ *isnull = true;
+ }
+
+ if (array_contains_nulls(arr))
+ {
+ ereport(elevel,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("\"%s\" array cannot contain NULL values", staname)));
+ *isnull = true;
+ }
+}
+
+/*
+ * Enforce parameter pairs that must be specified together for a particular
+ * stakind, such as most_common_vals and most_common_freqs for
+ * STATISTIC_KIND_MCV. If one is NULL and the other is not, emit at elevel,
+ * and ignore the stakind by setting both to NULL.
+ */
+static void
+check_arg_pair(const char *arg1name, bool *arg1null,
+ const char *arg2name, bool *arg2null,
+ int elevel)
+{
+ if (*arg1null && !*arg2null)
+ {
+ ereport(elevel,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("\"%s\" must be specified when \"%s\" is specified",
+ arg1name, arg2name)));
+ *arg2null = true;
+ }
+ if (!*arg1null && *arg2null)
+ {
+ ereport(elevel,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("\"%s\" must be specified when \"%s\" is specified",
+ arg2name, arg1name)));
+ *arg1null = true;
+ }
+}
+
/*
* Set statistics for a given pg_class entry.
*
@@ -205,3 +914,121 @@ pg_set_relation_stats(PG_FUNCTION_ARGS)
PG_RETURN_VOID();
}
+
+/*
+ * Import statistics for a given relation attribute.
+ *
+ * This will insert/replace a row in pg_statistic for the given relation and
+ * attribute name.
+ *
+ * The function takes input parameters that correspond to columns in the view
+ * pg_stats.
+ *
+ * Of those, the columns attname, inherited, null_frac, avg_width, and
+ * n_distinct all correspond to NOT NULL columns in pg_statistic. These
+ * parameters have no default value and passing NULL to them will result
+ * in an error.
+ *
+ * If there is no attribute with a matching attname in the relation, the
+ * function will raise an error. Likewise for setting inherited statistics
+ * on a table that is not partitioned.
+ *
+ * The remaining parameters all belong to a specific stakind. Some stakinds
+ * have multiple parameters, and in those cases both parameters must be
+ * NOT NULL or both NULL, otherwise an error will be raised.
+ *
+ * Omitting a parameter or explicitly passing NULL means that that particular
+ * stakind is not associated with the attribute.
+ *
+ * Parameters that are NOT NULL will be inspected for consistency checks,
+ * any of which can raise an error.
+ *
+ * Parameters corresponding to ANYARRAY columns are instead passed in as text
+ * values, which is a valid input string for an array of the type or element
+ * type of the attribute. Any error generated by the array_in() function will
+ * in turn fail the function.
+ */
+Datum
+pg_set_attribute_stats(PG_FUNCTION_ARGS)
+{
+ Oid reloid;
+ Name attname;
+ AttrNumber attnum;
+ int version = PG_VERSION_NUM;
+ int elevel = ERROR;
+ bool inherited;
+ float null_frac;
+ int avg_width;
+ float n_distinct;
+
+ if (PG_ARGISNULL(0))
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("relation cannot be NULL")));
+ return false;
+ }
+ reloid = PG_GETARG_OID(0);
+
+ if (PG_ARGISNULL(1))
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("attname cannot be NULL")));
+ return false;
+ }
+ attname = PG_GETARG_NAME(1);
+ attnum = get_attnum(reloid, NameStr(*attname));
+
+ if (PG_ARGISNULL(2))
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("inherited cannot be NULL")));
+ return false;
+ }
+ inherited = PG_GETARG_BOOL(2);
+
+ if (PG_ARGISNULL(3))
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("null_frac cannot be NULL")));
+ return false;
+ }
+ null_frac = PG_GETARG_FLOAT4(3);
+
+ if (PG_ARGISNULL(4))
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("avg_width cannot be NULL")));
+ return false;
+ }
+ avg_width = PG_GETARG_INT32(4);
+
+ if (PG_ARGISNULL(5))
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("n_distinct cannot be NULL")));
+ return false;
+ }
+ n_distinct = PG_GETARG_FLOAT4(5);
+
+ attribute_statistics_update(
+ reloid, attnum, version, elevel, inherited,
+ null_frac, avg_width, n_distinct,
+ PG_GETARG_DATUM(6), PG_ARGISNULL(6),
+ PG_GETARG_DATUM(7), PG_ARGISNULL(7),
+ PG_GETARG_DATUM(8), PG_ARGISNULL(8),
+ PG_GETARG_DATUM(9), PG_ARGISNULL(9),
+ PG_GETARG_DATUM(10), PG_ARGISNULL(10),
+ PG_GETARG_DATUM(11), PG_ARGISNULL(11),
+ PG_GETARG_DATUM(12), PG_ARGISNULL(12),
+ PG_GETARG_DATUM(13), PG_ARGISNULL(13),
+ PG_GETARG_DATUM(14), PG_ARGISNULL(14),
+ PG_GETARG_DATUM(15), PG_ARGISNULL(15));
+
+ PG_RETURN_VOID();
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index d700dd50f7b..fbd1a8a384d 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12263,5 +12263,12 @@
proargtypes => 'oid int4 float4 int4',
proargnames => '{relation,relpages,reltuples,relallvisible}',
prosrc => 'pg_set_relation_stats' },
+{ oid => '8049',
+ descr => 'set statistics on attribute',
+ proname => 'pg_set_attribute_stats', provolatile => 'v', proisstrict => 'f',
+ proparallel => 'u', prorettype => 'void',
+ proargtypes => 'oid name bool float4 int4 float4 text _float4 text float4 text _float4 _float4 text float4 text',
+ proargnames => '{relation,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}',
+ prosrc => 'pg_set_attribute_stats' },
]
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 7f2bf18716d..73d3b541dda 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,4 +127,7 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
int nclauses);
extern HeapTuple statext_expressions_load(Oid stxoid, bool inh, int idx);
+extern Datum pg_set_relation_stats(PG_FUNCTION_ARGS);
+extern Datum pg_set_attribute_stats(PG_FUNCTION_ARGS);
+
#endif /* STATISTICS_H */
diff --git a/src/test/regress/expected/stats_import.out b/src/test/regress/expected/stats_import.out
index f727cce9765..008d3aa26a3 100644
--- a/src/test/regress/expected/stats_import.out
+++ b/src/test/regress/expected/stats_import.out
@@ -93,7 +93,650 @@ WHERE oid = 'stats_import.test'::regclass;
18 | 401 | 5
(1 row)
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => '0'::oid,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.1::real,
+ avg_width => 2::integer,
+ n_distinct => 0.3::real);
+ERROR: cache lookup failed for attribute 0 of relation 0
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => NULL::oid,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.1::real,
+ avg_width => 2::integer,
+ n_distinct => 0.3::real);
+ERROR: relation cannot be NULL
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => NULL::name,
+ inherited => false::boolean,
+ null_frac => 0.1::real,
+ avg_width => 2::integer,
+ n_distinct => 0.3::real);
+ERROR: attname cannot be NULL
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => NULL::boolean,
+ null_frac => 0.1::real,
+ avg_width => 2::integer,
+ n_distinct => 0.3::real);
+ERROR: inherited cannot be NULL
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => NULL::real,
+ avg_width => 2::integer,
+ n_distinct => 0.3::real);
+ERROR: null_frac cannot be NULL
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.1::real,
+ avg_width => NULL::integer,
+ n_distinct => 0.3::real);
+ERROR: avg_width cannot be NULL
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.1::real,
+ avg_width => 2::integer,
+ n_distinct => NULL::real);
+ERROR: n_distinct cannot be NULL
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.1::real,
+ avg_width => 2::integer,
+ n_distinct => 0.3::real);
+ pg_set_attribute_stats
+------------------------
+
+(1 row)
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_import.test'::regclass;
+ stanullfrac | stawidth | stadistinct
+-------------+----------+-------------
+ 0.1 | 2 | 0.3
+(1 row)
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ most_common_freqs => '{0.1,0.2,0.3}'::real[]
+ );
+ERROR: "most_common_vals" must be specified when "most_common_freqs" is specified
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ most_common_vals => '{1,2,3}'::text
+ );
+ERROR: "most_common_freqs" must be specified when "most_common_vals" is specified
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ most_common_vals => '{2023-09-30,2024-10-31,3}'::text,
+ most_common_freqs => '{0.2,0.1}'::real[]
+ );
+ERROR: invalid input syntax for type integer: "2023-09-30"
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ most_common_vals => '{2,four,3}'::text,
+ most_common_freqs => '{0.3,0.25,0.05}'::real[]
+ );
+ERROR: invalid input syntax for type integer: "four"
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ most_common_vals => '{2,1,3}'::text,
+ most_common_freqs => '{0.3,0.25,0.05}'::real[]
+ );
+ pg_set_attribute_stats
+------------------------
+
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+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 | id | f | 0.5 | 2 | -0.1 | {2,1,3} | {0.3,0.25,0.05} | | | | | | | |
+(1 row)
+
+-- warn: histogram elements null value
+-- this generates no warnings, but perhaps it should
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ histogram_bounds => '{1,NULL,3,4}'::text
+ );
+ERROR: "histogram_bounds" array cannot contain NULL values
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ histogram_bounds => '{1,2,3,4}'::text
+ );
+ pg_set_attribute_stats
+------------------------
+
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+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 | id | f | 0.5 | 2 | -0.1 | | | {1,2,3,4} | | | | | | |
+(1 row)
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ correlation => 0.5::real);
+ pg_set_attribute_stats
+------------------------
+
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+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 | id | f | 0.5 | 2 | -0.1 | | | | 0.5 | | | | | |
+(1 row)
+
+-- warn: scalars can't have mcelem
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ most_common_elems => '{1,3}'::text,
+ most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+ );
+ERROR: unable to determine element type of attribute "id"
+DETAIL: Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'tags'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ most_common_elems => '{one,two}'::text
+ );
+ERROR: "most_common_freqs" must be specified when "most_common_elems" is specified
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'tags'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
+ );
+ERROR: "most_common_elems" must be specified when "most_common_freqs" is specified
+-- ok: mcelem
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'tags'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ most_common_elems => '{one,three}'::text,
+ most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+ );
+ pg_set_attribute_stats
+------------------------
+
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+ 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 | tags | f | 0.5 | 2 | -0.1 | | | | | {one,three} | {0.3,0.2,0.2,0.3,0} | | | |
+(1 row)
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+ );
+ERROR: unable to determine element type of attribute "id"
+DETAIL: Cannot set STATISTIC_KIND_MCELEM or STATISTIC_KIND_DECHIST.
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'tags'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ elem_count_histogram => '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+ );
+ERROR: "elem_count_histogram" array cannot contain NULL values
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'tags'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+ );
+ pg_set_attribute_stats
+------------------------
+
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+ 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 | tags | f | 0.5 | 2 | -0.1 | | | | | | | {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1} | | |
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ range_empty_frac => 0.5::real,
+ range_length_histogram => '{399,499,Infinity}'::text
+ );
+ERROR: attribute "id" is not a range type
+DETAIL: Cannot set STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM or STATISTIC_KIND_BOUNDS_HISTOGRAM.
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'arange'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ range_length_histogram => '{399,499,Infinity}'::text
+ );
+ERROR: "range_empty_frac" must be specified when "range_length_histogram" is specified
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'arange'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ range_empty_frac => 0.5::real
+ );
+ERROR: "range_length_histogram" must be specified when "range_empty_frac" is specified
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'arange'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ range_empty_frac => 0.5::real,
+ range_length_histogram => '{399,499,Infinity}'::text
+ );
+ pg_set_attribute_stats
+------------------------
+
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+ 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 | arange | f | 0.5 | 2 | -0.1 | | | | | | | | {399,499,Infinity} | 0.5 |
+(1 row)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+ );
+ERROR: attribute "id" is not a range type
+DETAIL: Cannot set STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM or STATISTIC_KIND_BOUNDS_HISTOGRAM.
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'arange'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+ );
+ pg_set_attribute_stats
+------------------------
+
+(1 row)
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+ 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 | arange | f | 0.5 | 2 | -0.1 | | | | | | | | | | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+(1 row)
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'arange'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ most_common_vals => '{"[2,3)","[1,2)","[3,4)"}'::text,
+ most_common_freqs => '{0.3,0.25,0.05}'::real[],
+ histogram_bounds => '{"[1,2)","[2,3)","[3,4)","[4,5)"}'::text,
+ correlation => 1.1::real,
+ most_common_elems => '{3,1}'::text,
+ most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[],
+ range_empty_frac => -0.5::real,
+ range_length_histogram => '{399,499,Infinity}'::text,
+ range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+ );
+ERROR: maximum number of statistics slots exceeded: 6
+--
+-- 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
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_import.complex_type, int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
+-- Generate statistics on table with data
+ANALYZE stats_import.test;
+CREATE TABLE stats_import.test_clone ( LIKE stats_import.test );
+CREATE INDEX is_odd_clone ON stats_import.test_clone(((comp).a % 2 = 1));
+--
+-- Copy stats from test to test_clone, and is_odd to is_odd_clone
+--
+SELECT s.schemaname, s.tablename, s.attname, s.inherited
+FROM pg_catalog.pg_stats AS s
+CROSS JOIN LATERAL
+ pg_catalog.pg_set_attribute_stats(
+ relation => ('stats_import.' || s.tablename || '_clone')::regclass::oid,
+ attname => s.attname,
+ 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,
+ 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 IN ('test', 'is_odd')
+ORDER BY s.tablename, s.attname, s.inherited;
+ schemaname | tablename | attname | inherited
+--------------+-----------+---------+-----------
+ stats_import | is_odd | expr | f
+ stats_import | test | arange | f
+ stats_import | test | comp | f
+ stats_import | test | id | f
+ stats_import | test | name | f
+ stats_import | test | tags | f
+(6 rows)
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+ relname | num_stats
+--------------+-----------
+ is_odd | 1
+ is_odd_clone | 1
+ test | 5
+ test_clone | 5
+(4 rows)
+
+-- check test minus test_clone
+SELECT
+ a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+ s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+ s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+ s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+ s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+ s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+ s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+ s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass
+EXCEPT
+SELECT
+ a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+ s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+ s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+ s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+ s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+ s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+ s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+ s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check test_clone minus test
+SELECT
+ a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+ s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+ s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+ s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+ s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+ s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+ s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+ s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass
+EXCEPT
+SELECT
+ a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+ s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+ s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+ s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+ s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+ s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+ s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+ s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd minus is_odd_clone
+SELECT
+ a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+ s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+ s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+ s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+ s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+ s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+ s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+ s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass
+EXCEPT
+SELECT
+ a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+ s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+ s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+ s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+ s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+ s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+ s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+ s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
+-- check is_odd_clone minus is_odd
+SELECT
+ a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+ s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+ s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+ s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+ s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+ s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+ s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+ s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+ a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+ s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+ s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+ s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+ s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+ s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+ s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+ s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass;
+ attname | stainherit | stanullfrac | stawidth | stadistinct | stakind1 | stakind2 | stakind3 | stakind4 | stakind5 | staop1 | staop2 | staop3 | staop4 | staop5 | stacoll1 | stacoll2 | stacoll3 | stacoll4 | stacoll5 | stanumbers1 | stanumbers2 | stanumbers3 | stanumbers4 | stanumbers5 | sv1 | sv2 | sv3 | sv4 | sv5 | direction
+---------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----+-----------
+(0 rows)
+
DROP SCHEMA stats_import CASCADE;
-NOTICE: drop cascades to 2 other objects
+NOTICE: drop cascades to 3 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_clone
diff --git a/src/test/regress/sql/stats_import.sql b/src/test/regress/sql/stats_import.sql
index effd5b892bf..4fa6ea41519 100644
--- a/src/test/regress/sql/stats_import.sql
+++ b/src/test/regress/sql/stats_import.sql
@@ -76,4 +76,548 @@ SELECT relpages, reltuples, relallvisible
FROM pg_class
WHERE oid = 'stats_import.test'::regclass;
+-- error: object doesn't exist
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => '0'::oid,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.1::real,
+ avg_width => 2::integer,
+ n_distinct => 0.3::real);
+
+-- error: relation null
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => NULL::oid,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.1::real,
+ avg_width => 2::integer,
+ n_distinct => 0.3::real);
+
+-- error: attname null
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => NULL::name,
+ inherited => false::boolean,
+ null_frac => 0.1::real,
+ avg_width => 2::integer,
+ n_distinct => 0.3::real);
+
+-- error: inherited null
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => NULL::boolean,
+ null_frac => 0.1::real,
+ avg_width => 2::integer,
+ n_distinct => 0.3::real);
+
+-- error: null_frac null
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => NULL::real,
+ avg_width => 2::integer,
+ n_distinct => 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.1::real,
+ avg_width => NULL::integer,
+ n_distinct => 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.1::real,
+ avg_width => 2::integer,
+ n_distinct => NULL::real);
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.1::real,
+ avg_width => 2::integer,
+ n_distinct => 0.3::real);
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_import.test'::regclass;
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ most_common_freqs => '{0.1,0.2,0.3}'::real[]
+ );
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ most_common_vals => '{1,2,3}'::text
+ );
+
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ most_common_vals => '{2023-09-30,2024-10-31,3}'::text,
+ most_common_freqs => '{0.2,0.1}'::real[]
+ );
+
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ most_common_vals => '{2,four,3}'::text,
+ most_common_freqs => '{0.3,0.25,0.05}'::real[]
+ );
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ most_common_vals => '{2,1,3}'::text,
+ most_common_freqs => '{0.3,0.25,0.05}'::real[]
+ );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: histogram elements null value
+-- this generates no warnings, but perhaps it should
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ histogram_bounds => '{1,NULL,3,4}'::text
+ );
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ histogram_bounds => '{1,2,3,4}'::text
+ );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ correlation => 0.5::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- warn: scalars can't have mcelem
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ most_common_elems => '{1,3}'::text,
+ most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+ );
+
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'tags'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ most_common_elems => '{one,two}'::text
+ );
+
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'tags'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
+ );
+
+-- ok: mcelem
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'tags'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ most_common_elems => '{one,three}'::text,
+ most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[]
+ );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+ );
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'tags'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ elem_count_histogram => '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+ );
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'tags'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ elem_count_histogram => '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+ );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ range_empty_frac => 0.5::real,
+ range_length_histogram => '{399,499,Infinity}'::text
+ );
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'arange'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ range_length_histogram => '{399,499,Infinity}'::text
+ );
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'arange'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ range_empty_frac => 0.5::real
+ );
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'arange'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ range_empty_frac => 0.5::real,
+ range_length_histogram => '{399,499,Infinity}'::text
+ );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'id'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+ );
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'arange'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+ );
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+ relation => 'stats_import.test'::regclass,
+ attname => 'arange'::name,
+ inherited => false::boolean,
+ null_frac => 0.5::real,
+ avg_width => 2::integer,
+ n_distinct => -0.1::real,
+ most_common_vals => '{"[2,3)","[1,2)","[3,4)"}'::text,
+ most_common_freqs => '{0.3,0.25,0.05}'::real[],
+ histogram_bounds => '{"[1,2)","[2,3)","[3,4)","[4,5)"}'::text,
+ correlation => 1.1::real,
+ most_common_elems => '{3,1}'::text,
+ most_common_elem_freqs => '{0.3,0.2,0.2,0.3,0.0}'::real[],
+ range_empty_frac => -0.5::real,
+ range_length_histogram => '{399,499,Infinity}'::text,
+ range_bounds_histogram => '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+ );
+--
+-- 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
+-- stacollN values. Because oids are not stable across databases, we can only
+-- test this when the source and destination are on the same database
+-- instance. For that reason, we borrow and adapt a query found in fe_utils
+-- and used by pg_dump/pg_upgrade.
+--
+INSERT INTO stats_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_import.complex_type, int4range(1,4), array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_import.complex_type, int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type, int4range(-1,1), array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, int4range(0,100), NULL;
+
+CREATE INDEX is_odd ON stats_import.test(((comp).a % 2 = 1));
+
+-- Generate statistics on table with data
+ANALYZE stats_import.test;
+
+CREATE TABLE stats_import.test_clone ( LIKE stats_import.test );
+
+CREATE INDEX is_odd_clone ON stats_import.test_clone(((comp).a % 2 = 1));
+
+--
+-- Copy stats from test to test_clone, and is_odd to is_odd_clone
+--
+SELECT s.schemaname, s.tablename, s.attname, s.inherited
+FROM pg_catalog.pg_stats AS s
+CROSS JOIN LATERAL
+ pg_catalog.pg_set_attribute_stats(
+ relation => ('stats_import.' || s.tablename || '_clone')::regclass::oid,
+ attname => s.attname,
+ 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,
+ 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 IN ('test', 'is_odd')
+ORDER BY s.tablename, s.attname, s.inherited;
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+
+-- check test minus test_clone
+SELECT
+ a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+ s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+ s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+ s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+ s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+ s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+ s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+ s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass
+EXCEPT
+SELECT
+ a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+ s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+ s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+ s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+ s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+ s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+ s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+ s.stavalues5::text AS sv5, 'test' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass;
+
+-- check test_clone minus test
+SELECT
+ a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+ s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+ s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+ s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+ s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+ s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+ s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+ s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test_clone'::regclass
+EXCEPT
+SELECT
+ a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+ s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+ s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+ s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+ s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+ s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+ s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+ s.stavalues5::text AS sv5, 'test_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.test'::regclass;
+
+-- check is_odd minus is_odd_clone
+SELECT
+ a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+ s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+ s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+ s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+ s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+ s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+ s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+ s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass
+EXCEPT
+SELECT
+ a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+ s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+ s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+ s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+ s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+ s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+ s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+ s.stavalues5::text AS sv5, 'is_odd' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass;
+
+-- check is_odd_clone minus is_odd
+SELECT
+ a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+ s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+ s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+ s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+ s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+ s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+ s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+ s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd_clone'::regclass
+EXCEPT
+SELECT
+ a.attname, s.stainherit, s.stanullfrac, s.stawidth, s.stadistinct,
+ s.stakind1, s.stakind2, s.stakind3, s.stakind4, s.stakind5,
+ s.staop1, s.staop2, s.staop3, s.staop4, s.staop5,
+ s.stacoll1, s.stacoll2, s.stacoll3, s.stacoll4, s.stacoll5,
+ s.stanumbers1, s.stanumbers2, s.stanumbers3, s.stanumbers4, s.stanumbers5,
+ s.stavalues1::text AS sv1, s.stavalues2::text AS sv2,
+ s.stavalues3::text AS sv3, s.stavalues4::text AS sv4,
+ s.stavalues5::text AS sv5, 'is_odd_clone' AS direction
+FROM pg_statistic s
+JOIN pg_attribute a ON a.attrelid = s.starelid AND a.attnum = s.staattnum
+WHERE s.starelid = 'stats_import.is_odd'::regclass;
+
DROP SCHEMA stats_import CASCADE;
--
2.34.1