>
>
>
>
> Having thought about it a bit more, I generally like the idea of being
> able to just update one stat instead of having to update all of them at
> once (and therefore having to go look up what the other values currently
> are...).  That said, per below, perhaps making it strict is the better
> plan.
>

v8 has it as strict.


>
> > > Also, in some cases we allow the function to be called with a
> > > NULL but then make it a no-op rather than throwing an ERROR (eg, if the
> > > OID ends up being NULL).
> >
> > Thoughts on it emitting a WARN or NOTICE before returning false?
>
> Eh, I don't think so?
>
> Where this is coming from is that we can often end up with functions
> like these being called inside of larger queries, and having them spit
> out WARN or NOTICE will just make them noisy.
>
> That leads to my general feeling of just returning NULL if called with a
> NULL OID, as we would get with setting the function strict.
>

In which case we're failing nearly silently, yes, there is a null returned,
but we have no idea why there is a null returned. If I were using this
function manually I'd want to know what I did wrong, what parameter I
skipped, etc.


> Well, that code is for pg_statistic while I was looking at pg_class (in
> vacuum.c:1428-1443, where we track if we're actually changing anything
> and only make the pg_class change if there's actually something
> different):
>

I can do that, especially since it's only 3 tuples of known types, but my
reservations are summed up in the next comment.



> Not sure why we don't treat both the same way though ... although it's
> probably the case that it's much less likely to have an entire
> pg_statistic row be identical than the few values in pg_class.
>

That would also involve comparing ANYARRAY values, yuk. Also, a matched
record will never be the case when used in primary purpose of the function
(upgrades), and not a big deal in the other future cases (if we use it in
ANALYZE on foreign tables instead of remote table samples, users
experimenting with tuning queries under hypothetical workloads).




> Hmm, that's a valid point, so a NULL passed in would need to set that
> value actually to NULL, presumably.  Perhaps then we should have
> pg_set_relation_stats() be strict and have pg_set_attribute_stats()
> handles NULLs passed in appropriately, and return NULL if the relation
> itself or attname, or other required (not NULL'able) argument passed in
> cause the function to return NULL.
>

That's how I have relstats done in v8, and could make it do that for attr
stats.

(What I'm trying to drive at here is a consistent interface for these
> functions, but one which does a no-op instead of returning an ERROR on
> values being passed in which aren't allowable; it can be quite
> frustrating trying to get a query to work where one of the functions
> decides to return ERROR instead of just ignoring things passed in which
> aren't valid.)
>

I like the symmetry of a consistent interface, but we've already got an
asymmetry in that the pg_class update is done non-transactionally (like
ANALYZE does).

One persistent problem is that there is no _safe equivalent to ARRAY_IN, so
that can always fail on us, though it should only do so if the string
passed in wasn't a valid array input format, or the values in the array
can't coerce to the attribute's basetype.

I should also point out that we've lost the ability to check if the export
values were of a type, and if the destination column is also of that type.
That's a non-issue in binary upgrades, but of course if a field changed
from integers to text the histograms would now be highly misleading.
Thoughts on adding a typname parameter that the function uses as a cheap
validity check?

v8 attached, incorporating these suggestions plus those of Bharath and
Bertrand. Still no pg_dump.

As for pg_dump, I'm currently leading toward the TOC entry having either a
series of commands:

    SELECT pg_set_relation_stats('foo.bar'::regclass, ...);
pg_set_attribute_stats('foo.bar'::regclass, 'id'::name, ...); ...

Or one compound command

    SELECT pg_set_relation_stats(t.oid, ...)
         pg_set_attribute_stats(t.oid, 'id'::name, ...),
         pg_set_attribute_stats(t.oid, 'last_name'::name, ...),
         ...
    FROM (VALUES('foo.bar'::regclass)) AS t(oid);

The second one has the feature that if any one attribute fails, then the
whole update fails, except, of course, for the in-place update of pg_class.
This avoids having an explicit transaction block, but we could get that
back by having restore wrap the list of commands in a transaction block
(and adding the explicit lock commands) when it is safe to do so.
From bdfde573f4f79770439a1455c1cb337701eb20dc Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huin...@gmail.com>
Date: Mon, 11 Mar 2024 14:18:39 -0400
Subject: [PATCH v8] Create pg_set_relation_stats, pg_set_attribute_stats.

These functions will be used by pg_dump/restore and pg_upgrade to convey
relation and attribute statistics from the source database to the
target. This would be done instead of vacuumdb --analyze-in-stages.

Both functions take an oid to identify the target relation that will
receive the statistics. There is nothing requiring that relation to be
the same one as the one exported, though the statistics would have to
make sense in the context of the new relation. Typecasts for stavaluesN
parameters may fail if the destination column is not of the same type as
the source column.

The parameters of pg_set_attribute_stats identify the attribute by name
rather than by attnum. This is intentional because the column order may
be different in situations other than binary upgrades. For example,
dropped columns do not carry over in a restore.

The statistics imported by pg_set_attribute_stats are imported
transactionally like any other operation. However, pg_set_relation_stats
does it's update in-place, which is to say non-transactionally. This is
in line with what ANALYZE does to avoid table bloat in pg_class.

These functions also allows for tweaking of table statistics in-place,
allowing the user to inflate rowcounts, skew histograms, etc, to see
what those changes will evoke from the query planner.
---
 src/include/catalog/pg_proc.dat               |  15 +
 src/include/statistics/statistics.h           |   2 +
 src/backend/statistics/Makefile               |   3 +-
 src/backend/statistics/meson.build            |   1 +
 src/backend/statistics/statistics.c           | 399 ++++++++++++++++++
 .../regress/expected/stats_export_import.out  | 211 +++++++++
 src/test/regress/parallel_schedule            |   2 +-
 src/test/regress/sql/stats_export_import.sql  | 198 +++++++++
 doc/src/sgml/func.sgml                        |  95 +++++
 9 files changed, 924 insertions(+), 2 deletions(-)
 create mode 100644 src/backend/statistics/statistics.c
 create mode 100644 src/test/regress/expected/stats_export_import.out
 create mode 100644 src/test/regress/sql/stats_export_import.sql

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 291ed876fc..9fa685e1ba 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12167,4 +12167,19 @@
   proargtypes => 'int2',
   prosrc => 'gist_stratnum_identity' },
 
+# Statistics Import
+{ oid => '8048',
+  descr => 'set statistics on relation',
+  proname => 'pg_set_relation_stats', provolatile => 'v', proisstrict => 't',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'oid float4 int4 int4',
+  proargnames => '{relation,reltuples,relpages,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 => 'bool',
+  proargtypes => 'oid name bool float4 int4 float4 int2 int2 int2 int2 int2 _float4 _float4 _float4 _float4 _float4 text text text text text',
+  proargnames => '{relation,attname,stainherit,stanullfrac,stawidth,stadistinct,stakind1,stakind2,stakind3,stakind4,stakind5,stanumbers1,stanumbers2,stanumbers3,stanumbers4,stanumbers5,stavalues1,stavalues2,stavalues3,stavalues4,stavalues5}',
+  prosrc => 'pg_set_attribute_stats' },
 ]
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 7f2bf18716..1dddf96576 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,4 +127,6 @@ 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/backend/statistics/Makefile b/src/backend/statistics/Makefile
index 89cf8c2797..e4f8ab7c4f 100644
--- a/src/backend/statistics/Makefile
+++ b/src/backend/statistics/Makefile
@@ -16,6 +16,7 @@ OBJS = \
 	dependencies.o \
 	extended_stats.o \
 	mcv.o \
-	mvdistinct.o
+	mvdistinct.o \
+	statistics.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/statistics/meson.build b/src/backend/statistics/meson.build
index 73b29a3d50..331e82c776 100644
--- a/src/backend/statistics/meson.build
+++ b/src/backend/statistics/meson.build
@@ -5,4 +5,5 @@ backend_sources += files(
   'extended_stats.c',
   'mcv.c',
   'mvdistinct.c',
+  'statistics.c',
 )
diff --git a/src/backend/statistics/statistics.c b/src/backend/statistics/statistics.c
new file mode 100644
index 0000000000..78b4e728fb
--- /dev/null
+++ b/src/backend/statistics/statistics.c
@@ -0,0 +1,399 @@
+/*-------------------------------------------------------------------------
+ * statistics.c
+ *
+ *	  POSTGRES statistics import
+ *
+ * Code supporting the direct importation of relation statistics, similar to
+ * what is done by the ANALYZE command.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *       src/backend/statistics/statistics.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_type.h"
+#include "fmgr.h"
+#include "nodes/nodeFuncs.h"
+#include "parser/parse_oper.h"
+#include "statistics/statistics.h"
+#include "utils/builtins.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/syscache.h"
+
+/*
+ * Set statistics for a given pg_class entry.
+ *
+ * This does an in-place (i.e. non-transactional) update of pg_class, just as
+ * is done in ANALYZE.
+ *
+ */
+Datum
+pg_set_relation_stats(PG_FUNCTION_ARGS)
+{
+	enum
+	{
+		P_RELATION = 0,			/* oid */
+		P_RELTUPLES,			/* float4 */
+		P_RELPAGES,				/* int */
+		P_RELALLVISIBLE,		/* int */
+		P_NUM_PARAMS
+	};
+
+	Oid			relid;
+	Relation	rel;
+	HeapTuple	ctup;
+	Form_pg_class pgcform;
+	float4		reltuples;
+	int			relpages;
+	int			relallvisible;
+
+	/* The function is strict, but just to be safe */
+	Assert(!PG_ARGISNULL(P_RELATION) && !PG_ARGISNULL(P_RELTUPLES) &&
+		   !PG_ARGISNULL(P_RELPAGES) && !PG_ARGISNULL(P_RELALLVISIBLE));
+
+	relid = PG_GETARG_OID(P_RELATION);
+
+	/*
+	 * Open the relation, getting ShareUpdateExclusiveLock to ensure that no
+	 * other stat-setting operation can run on it concurrently.
+	 */
+	rel = table_open(RelationRelationId, ShareUpdateExclusiveLock);
+
+	ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+	if (!HeapTupleIsValid(ctup))
+		elog(ERROR, "pg_class entry for relid %u vanished during statistics import",
+			 relid);
+
+	pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+	reltuples = PG_GETARG_FLOAT4(P_RELTUPLES);
+	relpages = PG_GETARG_INT32(P_RELPAGES);
+	relallvisible = PG_GETARG_INT32(P_RELALLVISIBLE);
+
+	/* Do not update pg_class unless there is no meaningful change */
+	if ((pgcform->reltuples != reltuples)
+		|| (pgcform->relpages != relpages)
+		|| (pgcform->relallvisible != relallvisible))
+	{
+		pgcform->reltuples = PG_GETARG_FLOAT4(P_RELTUPLES);
+		pgcform->relpages = PG_GETARG_INT32(P_RELPAGES);
+		pgcform->relallvisible = PG_GETARG_INT32(P_RELALLVISIBLE);
+
+		heap_inplace_update(rel, ctup);
+	}
+
+	table_close(rel, ShareUpdateExclusiveLock);
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * Import statistics for a given relation attribute.
+ *
+ * This will insert/replace a row in pg_statistic for the given relation and
+ * attribute name.
+ *
+ * This function will return NULL if either the relation or the attname are
+ * NULL.
+ *
+ * If there is no attribute with a matching attname in the relation, the
+ * function will return false. If there is a matching attribute but the
+ * attribute is dropped, then function will return false.
+ *
+ * The function does not specify values for staopN or stacollN parameters
+ * because those values are determined by the corresponding stakindN value and
+ * the attribute's underlying datatype.
+ *
+ * The stavaluesN parameters are text values, and must be a valid input string
+ * for an array of the basetype 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)
+{
+	enum
+	{
+		P_RELATION = 0,			/* oid */
+		P_ATTNAME,				/* name */
+		P_STAINHERIT,			/* bool */
+		P_STANULLFRAC,			/* float4 */
+		P_STAWIDTH,				/* int32 */
+		P_STADISTINCT,			/* float4 */
+		P_STAKIND1,				/* int16 */
+		P_STAKIND2,				/* int16 */
+		P_STAKIND3,				/* int16 */
+		P_STAKIND4,				/* int16 */
+		P_STAKIND5,				/* int16 */
+		P_STANUMBERS1,			/* float4[], null */
+		P_STANUMBERS2,			/* float4[], null */
+		P_STANUMBERS3,			/* float4[], null */
+		P_STANUMBERS4,			/* float4[], null */
+		P_STANUMBERS5,			/* float4[], null */
+		P_STAVALUES1,			/* text, null */
+		P_STAVALUES2,			/* text, null */
+		P_STAVALUES3,			/* text, null */
+		P_STAVALUES4,			/* text, null */
+		P_STAVALUES5,			/* text, null */
+		P_NUM_PARAMS
+	};
+
+	/* names of columns that cannot be null */
+	const char *required_param_names[] = {
+		"relation",
+		"attname",
+		"stainherit",
+		"stanullfrac",
+		"stawidth",
+		"stadistinct",
+		"stakind1",
+		"stakind2",
+		"stakind3",
+		"stakind4",
+		"stakind5",
+	};
+
+	Oid			relid;
+	Name		attname;
+	Relation	rel;
+	HeapTuple	tuple;
+
+	Oid			typid;
+	int32		typmod;
+	Oid			typcoll;
+	Oid			eqopr;
+	Oid			ltopr;
+	Oid			basetypid;
+	Oid			baseeqopr;
+	Oid			baseltopr;
+
+	float4		stanullfrac;
+	int			stawidth;
+
+	Datum		values[Natts_pg_statistic] = {0};
+	bool		nulls[Natts_pg_statistic] = {false};
+
+	Relation	sd;
+	HeapTuple	oldtup;
+	CatalogIndexState indstate;
+	HeapTuple	stup;
+	Form_pg_attribute attr;
+
+
+	/*
+	 * This function is pseudo-strct in that a NULL for the relation oid or
+	 * the attribute name results is a NULL return value.
+	 */
+	if (PG_ARGISNULL(P_RELATION) || PG_ARGISNULL(P_ATTNAME))
+		PG_RETURN_NULL();
+
+	for (int i = P_STAINHERIT; i <= P_STAKIND5; i++)
+		if (PG_ARGISNULL(i))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be NULL", required_param_names[i])));
+
+	relid = PG_GETARG_OID(P_RELATION);
+	attname = PG_GETARG_NAME(P_ATTNAME);
+	rel = relation_open(relid, ShareUpdateExclusiveLock);
+	tuple = SearchSysCache2(ATTNAME,
+							ObjectIdGetDatum(relid),
+							NameGetDatum(attname));
+
+	if (!HeapTupleIsValid(tuple))
+	{
+		relation_close(rel, NoLock);
+		PG_RETURN_BOOL(false);
+	}
+
+	attr = (Form_pg_attribute) GETSTRUCT(tuple);
+	if (attr->attisdropped)
+	{
+		ReleaseSysCache(tuple);
+		relation_close(rel, NoLock);
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * 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.
+	 */
+	if ((rel->rd_rel->relkind == RELKIND_INDEX
+		 || (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+		&& (rel->rd_indexprs != NIL)
+		&& (rel->rd_index->indkey.values[attr->attnum - 1] == 0))
+	{
+		ListCell   *indexpr_item = list_head(rel->rd_indexprs);
+		Node	   *expr;
+
+		for (int i = 0; i < attr->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");
+
+		expr = (Node *) lfirst(indexpr_item);
+
+		typid = exprType(expr);
+		typmod = exprTypmod(expr);
+
+		/*
+		 * 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))
+			typcoll = attr->attcollation;
+		else
+			typcoll = exprCollation(expr);
+	}
+	else
+	{
+		typid = attr->atttypid;
+		typmod = attr->atttypmod;
+		typcoll = attr->attcollation;
+	}
+
+	get_sort_group_operators(typid, false, false, false,
+							 &ltopr, &eqopr, NULL, NULL);
+
+	basetypid = get_base_element_type(typid);
+
+	if (basetypid == InvalidOid)
+	{
+		/* type is its own base type */
+		basetypid = typid;
+		baseltopr = ltopr;
+		baseeqopr = eqopr;
+	}
+	else
+		get_sort_group_operators(basetypid, false, false, false,
+								 &baseltopr, &baseeqopr, NULL, NULL);
+
+	stanullfrac = PG_GETARG_FLOAT4(P_STANULLFRAC);
+	if ((stanullfrac < 0.0) || (stanullfrac > 1.0))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("stanullfrac %f is out of range 0.0 to 1.0", stanullfrac)));
+
+	stawidth = PG_GETARG_INT32(P_STAWIDTH);
+	if (stawidth < 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("stawidth %d must be >= 0", stawidth)));
+
+	values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(relid);
+	values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attr->attnum);
+	values[Anum_pg_statistic_stainherit - 1] = PG_GETARG_DATUM(P_STAINHERIT);
+	values[Anum_pg_statistic_stanullfrac - 1] = PG_GETARG_DATUM(P_STANULLFRAC);
+	values[Anum_pg_statistic_stawidth - 1] = PG_GETARG_DATUM(P_STAWIDTH);
+	values[Anum_pg_statistic_stadistinct - 1] = PG_GETARG_DATUM(P_STADISTINCT);
+
+	/* The remaining fields are all parallel arrays, so we iterate over them */
+	for (int k = 0; k < STATISTIC_NUM_SLOTS; k++)
+	{
+		int16		kind = PG_GETARG_INT16(P_STAKIND1 + k);
+		Oid			opoid;
+		Oid			colloid;
+
+		switch (kind)
+		{
+			case 0:
+				opoid = InvalidOid;
+				colloid = InvalidOid;
+				break;
+			case STATISTIC_KIND_MCV:
+				opoid = eqopr;
+				colloid = typcoll;
+				break;
+			case STATISTIC_KIND_HISTOGRAM:
+			case STATISTIC_KIND_CORRELATION:
+			case STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM:
+			case STATISTIC_KIND_BOUNDS_HISTOGRAM:
+				opoid = ltopr;
+				colloid = typcoll;
+				break;
+			case STATISTIC_KIND_MCELEM:
+			case STATISTIC_KIND_DECHIST:
+				opoid = baseeqopr;
+				colloid = typcoll;
+				break;
+			default:
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("stakind%d = %d is out of the range 0 to %d", k + 1,
+								kind, STATISTIC_KIND_BOUNDS_HISTOGRAM)));
+		}
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] = PG_GETARG_DATUM(P_STAKIND1 + k);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(opoid);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(colloid);
+
+		if (PG_ARGISNULL(P_STANUMBERS1 + k))
+			nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		else
+			values[Anum_pg_statistic_stanumbers1 - 1 + k] = PG_GETARG_DATUM(P_STANUMBERS1 + k);
+
+		if (PG_ARGISNULL(P_STAVALUES1 + k))
+			nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+		else
+		{
+			char	   *s = TextDatumGetCString(PG_GETARG_DATUM(P_STAVALUES1 + k));
+			FmgrInfo	finfo;
+
+			fmgr_info(F_ARRAY_IN, &finfo);
+
+			values[Anum_pg_statistic_stavalues1 - 1 + k] =
+				FunctionCall3(&finfo, CStringGetDatum(s), ObjectIdGetDatum(basetypid),
+							  Int32GetDatum(typmod));
+
+			pfree(s);
+		}
+	}
+
+	sd = table_open(StatisticRelationId, RowExclusiveLock);
+
+	/* Is there already a pg_statistic tuple for this attribute? */
+	oldtup = SearchSysCache3(STATRELATTINH,
+							 ObjectIdGetDatum(relid),
+							 Int16GetDatum(attr->attnum),
+							 PG_GETARG_DATUM(P_STAINHERIT));
+
+	indstate = CatalogOpenIndexes(sd);
+
+	if (HeapTupleIsValid(oldtup))
+	{
+		/* Yes, replace it */
+		bool		replaces[Natts_pg_statistic] = {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);
+	relation_close(rel, NoLock);
+	ReleaseSysCache(tuple);
+	PG_RETURN_BOOL(true);
+}
diff --git a/src/test/regress/expected/stats_export_import.out b/src/test/regress/expected/stats_export_import.out
new file mode 100644
index 0000000000..56679c2615
--- /dev/null
+++ b/src/test/regress/expected/stats_export_import.out
@@ -0,0 +1,211 @@
+CREATE SCHEMA stats_export_import;
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b float,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    tags text[]
+);
+SELECT reltuples, relpages, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ reltuples | relpages | relallvisible 
+-----------+----------+---------------
+        -1 |        0 |             0
+(1 row)
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, 3.6::float4, 15000, 999);
+ pg_set_relation_stats 
+-----------------------
+ t
+(1 row)
+
+SELECT reltuples, relpages, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ reltuples | relpages | relallvisible 
+-----------+----------+---------------
+       3.6 |    15000 |           999
+(1 row)
+
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type, array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, NULL;
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ t
+(1 row)
+
+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_export_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         |         4
+ test_clone   |         4
+(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_export_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_export_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_export_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_export_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_export_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_export_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_export_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_export_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)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 1d8a414eea..0c89ffc02d 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -103,7 +103,7 @@ test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combo
 # ----------
 # Another group of parallel tests (JSON related)
 # ----------
-test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson
+test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson stats_export_import
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/sql/stats_export_import.sql b/src/test/regress/sql/stats_export_import.sql
new file mode 100644
index 0000000000..01087c9aee
--- /dev/null
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -0,0 +1,198 @@
+CREATE SCHEMA stats_export_import;
+
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b float,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    tags text[]
+);
+
+SELECT reltuples, relpages, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, 3.6::float4, 15000, 999);
+
+SELECT reltuples, relpages, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type, array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, NULL;
+
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+
+CREATE TABLE stats_export_import.test_clone ( LIKE stats_export_import.test );
+
+CREATE INDEX is_odd_clone ON stats_export_import.test_clone(((comp).a % 2 = 1));
+
+--
+-- Turn off ECHO for the transfer, because the actual stats generated by
+-- ANALYZE could change, and we don't care about the actual stats, we care
+-- about the ability to transfer them to another relation.
+--
+\set orig_ECHO :ECHO
+\set ECHO none
+
+-- copy stats from test to test_clone and is_odd to is_odd_clone
+SELECT
+    format('SELECT pg_catalog.pg_set_attribute_stats( '
+           || 'relation => %L::regclass, attname => %L::name, '
+           || 'stainherit => %L::boolean, stanullfrac => %s::real, '
+           || 'stawidth => %s::integer, stadistinct => %s::real, '
+           || 'stakind1 => %s::smallint, stakind2 => %s::smallint, '
+           || 'stakind3 => %s::smallint, stakind4 => %s::smallint, '
+           || 'stakind5 => %s::smallint, '
+           || 'stanumbers1 => %L::real[], stanumbers2 => %L::real[], '
+           || 'stanumbers3 => %L::real[], stanumbers4 => %L::real[], '
+           || 'stanumbers5 => %L::real[], '
+           || 'stavalues1 => %L::text, stavalues2 => %L::text, '
+           || 'stavalues3 => %L::text, stavalues4 => %L::text, '
+           || 'stavalues5 => %L::text)',
+        'stats_export_import.' || c.relname || '_clone', a.attname,
+        s.stainherit, s.stanullfrac,
+        s.stawidth, s.stadistinct,
+        s.stakind1, s.stakind2, s.stakind3,
+        s.stakind4, s.stakind5,
+        s.stanumbers1, s.stanumbers2,
+        s.stanumbers3, s.stanumbers4,
+        s.stanumbers5,
+        s.stavalues1::text, s.stavalues2::text, s.stavalues3::text,
+        s.stavalues4::text, s.stavalues5::text)
+FROM pg_class AS c
+JOIN pg_attribute a ON a.attrelid = c.oid
+JOIN pg_statistic s ON s.starelid = a.attrelid AND s.staattnum = a.attnum
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'is_odd')
+\gexec
+
+-- restore ECHO to original value
+\set ECHO :orig_ECHO
+
+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_export_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_export_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_export_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_export_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_export_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_export_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_export_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_export_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_export_import.is_odd'::regclass;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 0bb7aeb40e..76774cad18 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -28739,6 +28739,101 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
     in identifying the specific disk files associated with database objects.
    </para>
 
+   <table id="functions-admin-statsimport">
+    <title>Database Object Statistics Import Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_set_relation_stats</primary>
+        </indexterm>
+        <function>pg_set_relation_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>reltuples</parameter> <type>float4</type>,
+         <parameter>relpages</parameter> <type>integer</type> )
+         <parameter>relallvisible</parameter> <type>integer</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Updates the <structname>pg_class</structname> row for the specified
+        <parameter>relation</parameter>, setting the values for the columns
+        <structfield>reltuples</structfield>,
+        <structfield>relpages</structfield>, and
+        <structfield>relallvisible</structfield>.
+        To avoid table bloat in <structname>pg_class</structname>, this change
+        is made with an in-place update, and therefore cannot be rolled back through
+        normal transaction processing.
+       </para>
+       <para>
+        The purpose of this function is to apply statistics values in an
+        upgrade situation that are "good enough" for system operation until
+        they are replaced by the next auto-analyze. This function is used by
+        <command>pg_upgrade</command> and <command>pg_restore</command> to
+        convey the statistics from the old system version into the new one.
+       </para>
+       </entry>
+       <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>stainherit</parameter> <type>boolean</type>,
+         <parameter>stanullfrac</parameter> <type>real</type>,
+         <parameter>stawidth</parameter> <type>integer</type>,
+         <parameter>stadistinct</parameter> <type>real</type>,
+         <parameter>stakind1</parameter> <type>smallint</type>,
+         <parameter>stakind2</parameter> <type>smallint</type>,
+         <parameter>stakind3</parameter> <type>smallint</type>,
+         <parameter>stakind4</parameter> <type>smallint</type>,
+         <parameter>stakind5</parameter> <type>smallint</type>,
+         <parameter>stanumbers1</parameter> <type>real[]</type>,
+         <parameter>stanumbers2</parameter> <type>real[]</type>,
+         <parameter>stanumbers3</parameter> <type>real[]</type>,
+         <parameter>stanumbers4</parameter> <type>real[]</type>,
+         <parameter>stanumbers5</parameter> <type>real[]</type>,
+         <parameter>stavalues1</parameter> <type>text</type>,
+         <parameter>stavalues2</parameter> <type>text</type>,
+         <parameter>stavalues3</parameter> <type>text</type>,
+         <parameter>stavalues4</parameter> <type>text</type>,
+         <parameter>stavalues5</parameter> <type>text</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Replaces the <structname>pg_statistic</structname> row for the
+        <structname>pg_attribute</structname> row specified by
+        <parameter>relation</parameter> and <parameter>attname</parameter>.
+        The values for fields <structfield>staopN</structfield> and
+        <structfield>stacollN</structfield> are derived from the
+        <structname>pg_attribute</structname> row and the corresponding
+        <parameter>stakindN</parameter> value.
+       </para>
+       <para>
+        The purpose of this function is to apply statistics values in an
+        upgrade situation that are "good enough" for system operation until
+        they are replaced by the next auto-analyze. This function is used by
+        <command>pg_upgrade</command> and <command>pg_restore</command> to
+        convey the statistics from the old system version into the new one.
+       </para>
+       </entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
-- 
2.44.0

Reply via email to