From 210c45ac73361b33834aed5af2c90f3d53d345e7 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Thu, 21 Mar 2024 18:04:54 -0400
Subject: [PATCH v15 1/2] 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 have to make
sense in the context of the new relation.

The statistics provided will be checked for suitability and consistency,
and will raise an error if found.

The parameters of pg_set_attribute_stats intentionaly mirror the columns
in the view pg_stats, with the ANYARRAY types casted to TEXT. Those
values will be cast to arrays of the element type of the attribute, and that
operation may fail if the attribute type has changed. All stakind-based
statistics parameters has a default value of NULL.

In addition, the values provided are checked for consistency where
possible (values in acceptable ranges, array values in sub-ranges
defined within the array itself, array values in order, etc).

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/catalog/system_functions.sql      |   18 +
 src/backend/statistics/Makefile               |    3 +-
 src/backend/statistics/meson.build            |    1 +
 src/backend/statistics/statistics.c           | 1289 +++++++++++++++++
 .../regress/expected/stats_export_import.out  |  942 ++++++++++++
 src/test/regress/parallel_schedule            |    3 +-
 src/test/regress/sql/stats_export_import.sql  |  836 +++++++++++
 doc/src/sgml/func.sgml                        |  122 ++
 10 files changed, 3229 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 07023ee61d..5dbb291410 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12192,4 +12192,19 @@
   proargtypes => 'int2',
   prosrc => 'gist_stratnum_identity' },
 
+# Statistics Import
+{ oid => '8048',
+  descr => 'set statistics on relation',
+  proname => 'pg_set_relation_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'void',
+  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 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/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index fe2bb50f46..22be7e6653 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -636,6 +636,24 @@ 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/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..04868c587f
--- /dev/null
+++ b/src/backend/statistics/statistics.c
@@ -0,0 +1,1289 @@
+/*-------------------------------------------------------------------------
+ * 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/pg_collation.h"
+#include "catalog/pg_database.h"
+#include "catalog/pg_operator.h"
+#include "catalog/pg_type.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_type.h"
+#include "fmgr.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/float.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/rangetypes.h"
+#include "utils/syscache.h"
+#include "utils/typcache.h"
+
+/*
+ * A role has privileges to vacuum or analyze the relation if any of the
+ * following are true:
+ *   - the role owns the current database and the relation is not shared
+ *   - the role has the MAINTAIN privilege on the relation
+ *
+ */
+static bool
+can_modify_relation(Oid relid, Form_pg_class reltuple)
+{
+	return ((object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId())
+			 && !reltuple->relisshared) ||
+			pg_class_aclcheck(relid, GetUserId(), ACL_MAINTAIN) == ACLCHECK_OK);
+}
+
+/*
+ * A more encapsulated version of can_modify_relation for when the the
+ * HeapTuple and Form_pg_class are not needed later.
+ */
+static void
+check_relation_permissions(Relation rel)
+{
+	Oid			relid = RelationGetRelid(rel);
+	HeapTuple	ctup;
+	Form_pg_class pgcform;
+
+	/* Test existence of Relation */
+	ctup = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+
+	if (!HeapTupleIsValid(ctup))
+		elog(ERROR, "cache lookup failed for relation %u", relid);
+
+	pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+	if (!can_modify_relation(relid, pgcform))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(rel))));
+
+	ReleaseSysCache(ctup);
+}
+
+/*
+ * 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)
+{
+	/* Convenience enum to simulate naming the function arguments */
+	enum
+	{
+		P_RELATION = 0,			/* oid */
+		P_RELPAGES,				/* int */
+		P_RELTUPLES,			/* float4 */
+		P_RELALLVISIBLE,		/* int */
+		P_NUM_PARAMS
+	};
+
+	const char *param_names[] = {
+		"relation",
+		"relpages",
+		"reltuples",
+		"relallvisible"
+	};
+
+	Oid			relid;
+	Relation	rel;
+	HeapTuple	ctup;
+	Form_pg_class pgcform;
+	float4		reltuples;
+	int			relpages;
+	int			relallvisible;
+
+	/* Any NULL parameter is an error */
+	for (int i = P_RELATION; i < P_NUM_PARAMS; i++)
+		if (PG_ARGISNULL(i))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be NULL", param_names[i])));
+
+	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))
+		ereport(ERROR,
+				(errcode(ERRCODE_OBJECT_IN_USE),
+				 errmsg("pg_class entry for relid %u not found", relid)));
+
+	pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+	if (!can_modify_relation(relid, pgcform))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(rel))));
+
+
+	relpages = PG_GETARG_INT32(P_RELPAGES);
+	if (relpages < -1)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be < -1", param_names[P_RELPAGES])));
+	reltuples = PG_GETARG_FLOAT4(P_RELTUPLES);
+	if (reltuples < -1.0)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be < -1.0", param_names[P_RELTUPLES])));
+	relallvisible = PG_GETARG_INT32(P_RELALLVISIBLE);
+	if (relallvisible < -1)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be < -1", param_names[P_RELALLVISIBLE])));
+
+	/* Only update pg_class if there is a meaningful change */
+	if ((pgcform->reltuples != reltuples)
+		|| (pgcform->relpages != relpages)
+		|| (pgcform->relallvisible != relallvisible))
+	{
+		pgcform->relpages = PG_GETARG_INT32(P_RELPAGES);
+		pgcform->reltuples = PG_GETARG_FLOAT4(P_RELTUPLES);
+		pgcform->relallvisible = PG_GETARG_INT32(P_RELALLVISIBLE);
+
+		heap_inplace_update(rel, ctup);
+	}
+
+	table_close(rel, NoLock);
+
+	PG_RETURN_VOID();
+}
+
+
+
+/*
+ * Perform the cast of text to some array type
+ */
+static Datum
+cast_stavalues(FmgrInfo *finfo, Datum d, Oid typid, int32 typmod)
+{
+	char	   *s = TextDatumGetCString(d);
+	Datum		out = FunctionCall3(finfo, CStringGetDatum(s),
+									ObjectIdGetDatum(typid),
+									Int32GetDatum(typmod));
+
+	pfree(s);
+
+	return out;
+}
+
+/*
+ * Convenience routine to handle a common pattern where two function
+ * parameters must either both be NULL or both NOT NULL.
+ */
+static bool
+has_arg_pair(FunctionCallInfo fcinfo, const char **pnames, int p1, int p2)
+{
+	/* if on param is NULL and the other NOT NULL, report an error */
+	if (PG_ARGISNULL(p1) != PG_ARGISNULL(p2))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL when %s is NOT NULL",
+						pnames[(PG_ARGISNULL(p1)) ? p1 : p2],
+						pnames[(PG_ARGISNULL(p1)) ? p2 : p1])));
+
+	return (!PG_ARGISNULL(p1));
+}
+
+/*
+ * Test if the type is a scalar for MCELEM purposes
+ */
+static bool
+type_is_scalar(Oid typid)
+{
+	HeapTuple	tp;
+	bool		result = false;
+
+	tp = SearchSysCache1(TYPEOID, ObjectIdGetDatum(typid));
+	if (HeapTupleIsValid(tp))
+	{
+		Form_pg_type typtup = (Form_pg_type) GETSTRUCT(tp);
+
+		result = (!OidIsValid(typtup->typanalyze));
+		ReleaseSysCache(tp);
+	}
+	return result;
+}
+
+static int
+value_array_len(ExpandedArrayHeader *arr, const char *name)
+{
+	if (arr->ndims != 1)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be a multidimensional array", name)));
+
+	return arr->dims[0];
+}
+
+/*
+ * Convenience routine to encapsulate all of the steps needed for any
+ * value array.
+ */
+static int
+value_not_null_array_len(ExpandedArrayHeader *arr, const char *name)
+{
+	const int	nelems = value_array_len(arr, name);
+
+	if (nelems > 0)
+	{
+		deconstruct_expanded_array(arr);
+
+		/* if there's a nulls array, all values must be false */
+		if (arr->dnulls != NULL)
+			for (int i = 0; i < nelems; i++)
+				if (arr->dnulls[i])
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("%s array cannot contain NULL values", name)));
+	}
+
+	return nelems;
+}
+
+/*
+ * Fetch datatype information, this is needed to derive the proper staopN
+ * and stacollN values.
+ *
+ * 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 TypeCacheEntry *
+get_attr_stat_type(Relation rel, Name attname,
+				   int16 *attnum, int32 *typmod, Oid *typcoll)
+{
+	Oid			relid = RelationGetRelid(rel);
+	Form_pg_attribute attr;
+	HeapTuple	atup;
+	Oid			typid;
+
+	atup = SearchSysCache2(ATTNAME, ObjectIdGetDatum(relid),
+						   NameGetDatum(attname));
+
+	/* Attribute not found */
+	if (!HeapTupleIsValid(atup))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s has no attname %s",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+
+	attr = (Form_pg_attribute) GETSTRUCT(atup);
+	if (attr->attisdropped)
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s attname %s is dropped",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+
+	*attnum = attr->attnum;
+
+	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;
+	}
+	ReleaseSysCache(atup);
+
+	/* if it's a multirange, step down to the range type */
+	if (type_is_multirange(typid))
+		typid = get_multirange_range(typid);
+
+	return lookup_type_cache(typid, 
+							 TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR);
+}
+
+
+/*
+ * The null_frac statistic must be in [0.0,1.0].
+ */
+static void
+validate_null_frac(float4 null_frac, const char *statname)
+{
+	const float4 min = 0.0;
+	const float4 max = 1.0;
+
+	if ((null_frac < min) || (null_frac > max))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %f is out of range %.1f to %.1f",
+						statname, null_frac, min, max)));
+}
+
+/*
+ * The avg_width statistic must be non-negative.
+ */
+static void
+validate_avg_width(int32 avg_width, const char *statname)
+{
+	const int	min = 0;
+
+	if (avg_width < min)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %d must be >= %d", statname, avg_width, min)));
+}
+
+/*
+ * The n_distinct statistic cannot be below -1.0.
+ */
+static void
+validate_n_distinct(float4 n_distinct, const char *statname)
+{
+	const float4 min = -1.0;
+
+	if (n_distinct < min)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %f must be >= %.1f",
+						statname, n_distinct, min)));
+}
+
+/*
+ * Check correctness of MCV statistics pair.
+ *
+ * The length of the freqs array must be equal to the length of the values
+ * array. Neither array can contain NULL elements.
+ *
+ * The elements in the freqs array must be monotonically nondecreasing, and the
+ * sum of values in the array theoretically should not exceed 1.0, but we use a
+ * more relaxed limit to allow for compounded rounding errors.
+ */
+static void
+validate_mcv(Datum freqs, Datum values, const char *freqsname,
+			 const char *valuesname)
+{
+	ExpandedArrayHeader *freqsarr = DatumGetExpandedArray(freqs);
+	ExpandedArrayHeader *valsarr = DatumGetExpandedArray(values);
+
+	int			nvals = value_array_len(valsarr, valuesname);
+	int			nfreqs = value_not_null_array_len(freqsarr, freqsname);
+
+	if (nfreqs != nvals)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s has %d elements, but %s has %d elements, "
+						"but they must be equal",
+						freqsname, nfreqs, valuesname, nvals)));
+
+	/*
+	 * check that freqs sum to <= 1.0 or some number slightly higer to allow
+	 * for compounded rounding errors.
+	 */
+	if (nfreqs >= 1)
+	{
+		const float4 freqsummax = 1.1;
+
+		float4		prev = DatumGetFloat4(freqsarr->dvalues[0]);
+		float4		freqsum = prev;
+
+		for (int i = 1; i < nfreqs; i++)
+		{
+			float4		f = DatumGetFloat4(freqsarr->dvalues[i]);
+
+			if (f > prev)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s array values must be in descending "
+								"order, but %f > %f",
+								freqsname, f, prev)));
+
+			freqsum += f;
+			prev = f;
+		}
+
+		if (freqsum > freqsummax)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("The sum of elements in %s must not exceed "
+							"%.2f but is %f",
+							freqsname, freqsummax, freqsum)));
+	}
+}
+
+/*
+ * Check correctness of Histogram Bounds statistics.
+ *
+ * The array represents a histogram, which means that the values must be in
+ * monotonically non-decreasing order.
+ *
+ * If the attribute datatype in question uses collations then this validation
+ * has the chance to turn up any discrepancies in the source and destination
+ * collations if the datatype uses collations.
+ */
+static void
+validate_histogram_bounds(Datum stavalues, Datum srcstrvalue,
+						  Oid ltopr, Oid typcollation,
+						  const char *statname)
+{
+	ExpandedArrayHeader *arr = DatumGetExpandedArray(stavalues);
+	SortSupportData ssupd;
+
+	int			nelems = value_not_null_array_len(arr, statname);
+
+	memset(&ssupd, 0, sizeof(ssupd));
+	ssupd.ssup_cxt = CurrentMemoryContext;
+	ssupd.ssup_collation = typcollation;
+	ssupd.ssup_nulls_first = false;
+	ssupd.abbreviate = false;
+
+	PrepareSortSupportFromOrderingOp(ltopr, &ssupd);
+
+	/*
+	 * Array should be in * monotonically non-decreasing order. If we every
+	 * find a case where [n] > [n+1], raise an error.
+	 */
+	for (int i = 1; i < nelems; i++)
+	{
+		Datum		a = arr->dvalues[i - 1];
+		Datum		b = arr->dvalues[i];
+
+		if (ssupd.comparator(a, b, &ssupd) > 0)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s values must be in ascending order %s",
+							statname, TextDatumGetCString(srcstrvalue))));
+
+	}
+}
+
+/*
+ * Check correctness of Correlation statistics.
+ *
+ * Correlation must be in [-1.0,1,0].
+ */
+static void
+validate_correlation(float4 corr, const char *statname)
+{
+	const float4 min = -1.0;
+	const float4 max = 1.0;
+
+	if ((corr < min) || (corr > max))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %f is out of range %.1f to %.1f",
+						statname, corr, min, max)));
+}
+
+/*
+ * Check correctness of Most Common Elements statistics.
+ *
+ * Neither array can contain NULL elements.
+ *
+ * The values array must be in monotonically incresing order. Elements may
+ * not repeat.
+ *
+ * Every element in the values array must have a corresponding element in the
+ * freqs array. The freqs array will have two additional elements, representing
+ * the frequency lower bound (LB) and frequency upper bound UB). The freqs
+ * array may have an optoinal extra value representing the null fraction of
+ * values in the column.
+ *
+ * The null fraction, LB and UB must be between [0.0,1.0].
+ *
+ * The LB must be <= the UB.
+ */
+static void
+validate_most_common_elements(Datum freqs, Datum elements, Datum srcstrvalue,
+							  const char *freqsname, const char *elemsname,
+							  Oid ltopr, Oid typcollation)
+{
+	ExpandedArrayHeader *freqsarr = DatumGetExpandedArray(freqs);
+	ExpandedArrayHeader *elemsarr = DatumGetExpandedArray(elements);
+
+	int			nfreqs = value_not_null_array_len(freqsarr, freqsname);
+	int			nelems = value_not_null_array_len(elemsarr, elemsname);
+
+	/*
+	 * The mcelem freqs array has either 2 or 3 additional values: the min
+	 * frequency, the max frequency, the optional null frequency.
+	 */
+	const int	nfreqsmin = nelems + 2;
+	const int	nfreqsmax = nelems + 3;
+
+	/*
+	 * the freqlowbound and freqhighbound must themselves be valid percentages
+	 */
+	const float4 frac_min = 0.0;
+	const float4 frac_max = 1.0;
+
+	float4		freqlowbound;
+	float4		freqhighbound;
+
+	if (nfreqs <= 0)
+		return;
+
+	if ((nfreqs < nfreqsmin) || (nfreqs > nfreqsmax))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s has %d elements, but must have between "
+						"%d and %d because %s has %d elements",
+						freqsname, nfreqs, nfreqsmin, nfreqsmax,
+						elemsname, nelems)));
+
+	/* first freq element past the length of the values is the min */
+	freqlowbound = DatumGetFloat4(freqsarr->dvalues[nelems]);
+	if ((freqlowbound < frac_min) || (freqlowbound > frac_max))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %s frequency %f is out of "
+						"range %.1f to %.1f",
+						freqsname, "minimum", freqlowbound,
+						frac_min, frac_max)));
+
+	/* second freq element past the length of the values is the max */
+	freqhighbound = DatumGetFloat4(freqsarr->dvalues[nelems + 1]);
+	if ((freqhighbound < frac_min) || (freqhighbound > frac_max))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %s frequency %f is out of "
+						"range %.1f to %.1f",
+						freqsname, "maximum", freqhighbound,
+						frac_min, frac_max)));
+
+	/* low bound must be < high bound */
+	if (freqlowbound > freqhighbound)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s frequency low bound %f cannot be greater "
+						"than high bound %f",
+						freqsname, freqlowbound, freqhighbound)));
+
+	/*
+	 * third freq element past the length of the values is the null frac
+	 */
+	if (nfreqs == nelems + 3)
+	{
+		float4		freqnullpct;
+
+		freqnullpct = DatumGetFloat4(freqsarr->dvalues[nelems + 2]);
+
+		if ((freqnullpct < frac_min) || (freqnullpct > frac_max))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s %s frequency %f is out of "
+							"range %.1f to %.1f",
+							freqsname, "null", freqnullpct,
+							frac_min, frac_max)));
+	}
+
+	/*
+	 * All the freqs that match up to a val must be between low/high bounds
+	 * (which is never less strict than frac_min/frac_max)
+	 *
+	 * Also, these frequencies do not sum to a number <= 1.0 as is the case
+	 * with MC_FREQS.
+	 */
+	for (int i = 0; i < nelems; i++)
+	{
+		float4		f = DatumGetFloat4(freqsarr->dvalues[i]);
+
+		if ((f < freqlowbound) || (f > freqhighbound))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s frequency %f is out of range %f to %f",
+							freqsname, f, freqlowbound, freqhighbound)));
+	}
+
+	/*
+	 * All the elements must be unique and in ascending order.
+	 */
+	if (nelems > 1)
+	{
+		SortSupportData ssupd;
+
+		memset(&ssupd, 0, sizeof(ssupd));
+		ssupd.ssup_cxt = CurrentMemoryContext;
+		ssupd.ssup_collation = typcollation;
+		ssupd.ssup_nulls_first = false;
+		ssupd.abbreviate = false;
+
+		PrepareSortSupportFromOrderingOp(ltopr, &ssupd);
+
+		for (int i = 1; i < nelems; i++)
+		{
+			Datum		a = elemsarr->dvalues[i - 1];
+			Datum		b = elemsarr->dvalues[i];
+
+			if (ssupd.comparator(a, b, &ssupd) >= 0)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s values must be unqiue and in ascending order %s",
+								elemsname, TextDatumGetCString(srcstrvalue))));
+
+		}
+	}
+}
+
+/*
+ * Check correctness of Distinct Elements Count Histogram statistics.
+ *
+ * All elements must be >= 0.0, and must be in monotonically nonincreasing
+ * order.
+ */
+static void
+validate_distinct_elements_count_histogram(Datum stanumbers,
+										   const char *statname)
+{
+	ExpandedArrayHeader *arr = DatumGetExpandedArray(stanumbers);
+
+	const float4 last_min = 0.0;
+
+	int			nelems = value_not_null_array_len(arr, statname);
+	float4		last;
+
+	/* Last element must be >= 0 */
+	last = DatumGetFloat4(arr->dvalues[nelems - 1]);
+	if (last < last_min)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s has last element %f < %.1f",
+						statname, last, last_min)));
+
+	/* all other elements must be monotonically nondecreasing */
+	if (nelems > 1)
+	{
+		float4		prev = DatumGetFloat4(arr->dvalues[0]);
+
+		for (int i = 1; i < nelems - 1; i++)
+		{
+			float4		f = DatumGetFloat4(arr->dvalues[i]);
+
+			if (f < prev)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s array values must be in ascending "
+								"order, but %f > %f",
+								statname, prev, f)));
+
+			prev = f;
+		}
+	}
+}
+
+/*
+ * Validate Range Bound Histogram statistic.
+ *
+ * The values in this array are ranges of the same scalar type as the attribute
+ * in question and cannot be empty. However those ranges are being used to
+ * represent two parallel arrays with inclusive/exclusive bounds, representing
+ * the histogram of lower bounds and the histogram of upper bounds. Each of
+ * those two arrays must each be monotonically nondecreasing.
+ */
+
+static void
+validate_bounds_histogram(Datum stavalues, Datum strvalue,
+						  TypeCacheEntry *typcache, const char *statname)
+{
+	ExpandedArrayHeader *arr = DatumGetExpandedArray(stavalues);
+
+	int			nelems = value_not_null_array_len(arr, statname);
+	RangeType  *prevrange;
+	RangeBound	prevlower;
+	RangeBound	prevupper;
+	bool		empty;
+
+	if (nelems <= 0)
+		return;
+
+	/* check first element for emptiness */
+	prevrange = DatumGetRangeTypeP(arr->dvalues[0]);
+	range_deserialize(typcache, prevrange, &prevlower,
+					  &prevupper, &empty);
+
+	if (empty)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s array bounds cannot have empty elements",
+						statname)));
+
+	/*
+	 * For element N, verify that: lower_bound(N) <= lower_bound(N+1), and
+	 * upper_bound(N) <= upper_bound(N+1)
+	 */
+	for (int i = 1; i < nelems; i++)
+	{
+		RangeType  *range = DatumGetRangeTypeP(arr->dvalues[i]);
+		RangeBound	lower;
+		RangeBound	upper;
+
+		range_deserialize(typcache, range, &lower, &upper, &empty);
+
+		if (empty)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s array bounds cannot have empty elements",
+							statname)));
+
+		if (range_cmp_bounds(typcache, &prevlower, &lower) == 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s array %s bounds must be in ascending order",
+							statname, "lower")));
+
+		if (range_cmp_bounds(typcache, &prevupper, &upper) == 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s array %s bounds must be in ascending order",
+							statname, "upper")));
+
+		memcpy(&lower, &prevlower, sizeof(RangeBound));
+		memcpy(&upper, &prevupper, sizeof(RangeBound));
+	}
+}
+
+/*
+ * Validate Range Length Histogram statistic pair.
+ *
+ * The empty_frac must be between in [0.0,1.0].
+ *
+ * rlhist is a histogram of float8[], so it must be in monotonically
+ * nondecreasing order.
+ */
+static void
+validate_range_length_histogram(Datum rlhist, float4 empty_frac,
+								const char *histname,
+								const char *fracname)
+{
+	ExpandedArrayHeader *arr = DatumGetExpandedArray(rlhist);
+
+	int			nelems = value_not_null_array_len(arr, histname);
+
+	const float4 min = 0.0;
+	const float4 max = 1.0;
+
+	if ((empty_frac < min) || (empty_frac > max))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s %f is out of range %.1f to %.1f",
+						fracname, empty_frac, min, max)));
+
+	if (nelems > 1)
+	{
+		float8		prev = DatumGetFloat8(arr->dvalues[0]);
+
+		for (int i = 1; i < nelems; i++)
+		{
+			float8		f = DatumGetFloat8(arr->dvalues[i]);
+
+			if (f < prev)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s array values must be in ascending "
+								"order, but %f > %f",
+								histname, prev, f)));
+
+			prev = f;
+		}
+	}
+}
+
+/*
+ * 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, NoLock);
+}
+
+/*
+ * 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)
+{
+	/* Convenience enum to simulate naming the function arguments */
+	enum
+	{
+		P_RELATION = 0,			/* oid */
+		P_ATTNAME,				/* name */
+		P_INHERITED,			/* bool */
+		P_NULL_FRAC,			/* float4 */
+		P_AVG_WIDTH,			/* int32 */
+		P_N_DISTINCT,			/* float4 */
+		P_MC_VALS,				/* text, null */
+		P_MC_FREQS,				/* float4[], null */
+		P_HIST_BOUNDS,			/* text, null */
+		P_CORRELATION,			/* float4, null */
+		P_MC_ELEMS,				/* text, null */
+		P_MC_ELEM_FREQS,		/* float4[], null */
+		P_ELEM_COUNT_HIST,		/* float4[], null */
+		P_RANGE_LENGTH_HIST,	/* text, null */
+		P_RANGE_EMPTY_FRAC,		/* float4, null */
+		P_RANGE_BOUNDS_HIST,	/* text, null */
+		P_NUM_PARAMS
+	};
+
+	/* names of arguments indexed by above enum */
+	const char *param_names[] = {
+		"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"
+	};
+
+	Oid			relid;
+	Name		attname;
+	Relation	rel;
+
+	TypeCacheEntry *typcache;
+	TypeCacheEntry *basetypcache;
+
+	int16		attnum;
+	int32		typmod;
+	Oid			typcoll;
+
+	Datum		values[Natts_pg_statistic] = {0};
+	bool		nulls[Natts_pg_statistic] = {false};
+
+	FmgrInfo	finfo;
+
+	bool		has_mcv;
+	bool		has_mc_elems;
+	bool		has_rl_hist;
+	int			stakind_count;
+
+	int			k = 0;
+
+	/*
+	 * A null in a required parameter is an error.
+	 */
+	for (int i = P_RELATION; i <= P_N_DISTINCT; i++)
+		if (PG_ARGISNULL(i))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be NULL", param_names[i])));
+
+	/*
+	 * Check all parameter pairs up front.
+	 */
+	has_mcv = has_arg_pair(fcinfo, param_names,
+						   P_MC_VALS, P_MC_FREQS);
+	has_mc_elems = has_arg_pair(fcinfo, param_names,
+								P_MC_ELEMS, P_MC_ELEM_FREQS);
+	has_rl_hist = has_arg_pair(fcinfo, param_names,
+							   P_RANGE_LENGTH_HIST, P_RANGE_EMPTY_FRAC);
+
+	/*
+	 * If a caller specifies more stakind-stats than we have slots to store
+	 * them, raise an error.
+	 */
+	stakind_count = (int) has_mcv + (int) has_mc_elems + (int) has_rl_hist +
+		(int) !PG_ARGISNULL(P_HIST_BOUNDS) +
+		(int) !PG_ARGISNULL(P_CORRELATION) +
+		(int) !PG_ARGISNULL(P_ELEM_COUNT_HIST) +
+		(int) !PG_ARGISNULL(P_RANGE_BOUNDS_HIST);
+
+	if (stakind_count > STATISTIC_NUM_SLOTS)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("imported statistics must have a maximum of %d slots "
+						"but %d given",
+						STATISTIC_NUM_SLOTS, stakind_count)));
+
+	relid = PG_GETARG_OID(P_RELATION);
+
+	rel = relation_open(relid, ShareUpdateExclusiveLock);
+
+	check_relation_permissions(rel);
+
+	attname = PG_GETARG_NAME(P_ATTNAME);
+	typcache = get_attr_stat_type(rel, attname, &attnum, &typmod, &typcoll);
+
+	/*
+	 * Derive base type if we have stats kinds that need it.
+	 *
+	 * This duplicates some type-specific logic found in various typanalyze
+	 * functions which are called from vacuum's examine_attribute(), but
+	 * using that directly has proven awkward.
+	 */
+	if (has_mc_elems || !PG_ARGISNULL(P_ELEM_COUNT_HIST))
+	{
+		Oid basetypid;
+
+		if (typcache->type_id == TSVECTOROID)
+		{
+			/* tsvectors always have a text oid base type and default collation */
+			basetypid = TEXTOID;
+			typcoll = DEFAULT_COLLATION_OID;
+		}
+		else if (typcache->typtype == TYPTYPE_RANGE)
+			basetypid = get_range_subtype(typcache->type_id);
+		else
+			basetypid = get_base_element_type(typcache->type_id);
+
+		/* not finding a basetype means we already had it */
+		if (basetypid == InvalidOid)
+			basetypid = typcache->type_id;
+
+		/* The stats need the eq_opr, but validation needs the lt_opr */
+		basetypcache = lookup_type_cache(basetypid,
+										 TYPECACHE_LT_OPR | TYPECACHE_EQ_OPR);
+	}
+
+	/* P_HIST_BOUNDS and P_CORRELATION must have a type < operator */
+	if (typcache->lt_opr == InvalidOid)
+		for (int i = P_HIST_BOUNDS; i <= P_CORRELATION; i++)
+			if (!PG_ARGISNULL(i))
+				ereport(ERROR,
+						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+						 errmsg("Relation %s attname %s cannot "
+								"have stats of type %s",
+								RelationGetRelationName(rel),
+								NameStr(*attname),
+								param_names[i])));
+
+	/* Scalar types can't have P_MC_ELEMS, P_MC_ELEM_FREQS, P_ELEM_COUNT_HIST */
+	if (type_is_scalar(typcache->type_id))
+		for (int i = P_MC_ELEMS; i <= P_ELEM_COUNT_HIST; i++)
+			if (!PG_ARGISNULL(i))
+				ereport(ERROR,
+						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+						 errmsg("Relation %s attname %s is a scalar type, "
+								"cannot have stats of type %s",
+								RelationGetRelationName(rel),
+								NameStr(*attname),
+								param_names[i])));
+
+	/* Only range types can have P_RANGE_x */
+	if ((typcache->typtype != TYPTYPE_RANGE) &&
+		(typcache->typtype != TYPTYPE_RANGE))
+		for (int i = P_RANGE_LENGTH_HIST; i <= P_RANGE_BOUNDS_HIST; i++)
+			if (!PG_ARGISNULL(i))
+				ereport(ERROR,
+						(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+						 errmsg("Relation %s attname %s is not a range type, "
+								"cannot have stats of type %s",
+								RelationGetRelationName(rel),
+								NameStr(*attname),
+								param_names[i])));
+
+	validate_null_frac(PG_GETARG_FLOAT4(P_NULL_FRAC),
+					   param_names[P_NULL_FRAC]);
+	validate_avg_width(PG_GETARG_INT32(P_AVG_WIDTH),
+					   param_names[P_AVG_WIDTH]);
+	validate_n_distinct(PG_GETARG_FLOAT4(P_N_DISTINCT),
+						param_names[P_N_DISTINCT]);
+	values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(relid);
+	values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attnum);
+	values[Anum_pg_statistic_stainherit - 1] = PG_GETARG_DATUM(P_INHERITED);
+	values[Anum_pg_statistic_stanullfrac - 1] = PG_GETARG_DATUM(P_NULL_FRAC);
+	values[Anum_pg_statistic_stawidth - 1] = PG_GETARG_DATUM(P_AVG_WIDTH);
+	values[Anum_pg_statistic_stadistinct - 1] = PG_GETARG_DATUM(P_N_DISTINCT);
+
+	fmgr_info(F_ARRAY_IN, &finfo);
+
+	/* MC_VALS && MC_FREQS => STATISTIC_KIND_MCV */
+	if (has_mcv)
+	{
+		Datum		stanumbers = PG_GETARG_DATUM(P_MC_FREQS);
+		Datum		stavalues = cast_stavalues(&finfo, PG_GETARG_DATUM(P_MC_VALS),
+											   typcache->type_id, typmod);
+
+		validate_mcv(stanumbers, stavalues, param_names[P_MC_FREQS],
+					 param_names[P_MC_VALS]);
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_MCV);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(typcache->eq_opr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+		values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+		k++;
+	}
+
+	/* HIST_BOUNDS => STATISTIC_KIND_HISTOGRAM */
+	if (!PG_ARGISNULL(P_HIST_BOUNDS))
+	{
+		Datum		strvalue = PG_GETARG_DATUM(P_HIST_BOUNDS);
+		Datum		stavalues = cast_stavalues(&finfo, strvalue,
+											   typcache->type_id, typmod);
+
+		validate_histogram_bounds(stavalues, strvalue, typcache->lt_opr, typcoll,
+								  param_names[P_HIST_BOUNDS]);
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_HISTOGRAM);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(typcache->lt_opr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+		k++;
+	}
+
+	/* CORRELATION => STATISTIC_KIND_CORRELATION */
+	if (!PG_ARGISNULL(P_CORRELATION))
+	{
+		Datum		elem = PG_GETARG_DATUM(P_CORRELATION);
+		Datum		elems[] = {elem};
+		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+		Datum		stanumbers = PointerGetDatum(arry);
+
+		validate_correlation(DatumGetFloat4(elem), param_names[P_CORRELATION]);
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_CORRELATION);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(typcache->lt_opr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+		k++;
+	}
+
+	/* MC_ELEMS && MC_ELEM_FREQS => STATISTIC_KIND_MCELEM */
+	if (has_mc_elems)
+	{
+		Datum		srcstrvalue = PG_GETARG_DATUM(P_MC_ELEMS);
+		Datum		stanumbers = PG_GETARG_DATUM(P_MC_ELEM_FREQS);
+		Datum		stavalues = cast_stavalues(&finfo, srcstrvalue,
+											   basetypcache->type_id, typmod);
+
+		validate_most_common_elements(stanumbers, stavalues, srcstrvalue,
+									  param_names[P_MC_ELEM_FREQS],
+									  param_names[P_MC_ELEMS],
+									  basetypcache->lt_opr, typcoll);
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_MCELEM);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(basetypcache->eq_opr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+		values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+		k++;
+	}
+
+	/* ELEM_COUNT_HIST => STATISTIC_KIND_DECHIST */
+	if (!PG_ARGISNULL(P_ELEM_COUNT_HIST))
+	{
+		Datum		stanumbers = PG_GETARG_DATUM(P_ELEM_COUNT_HIST);
+
+		validate_distinct_elements_count_histogram(stanumbers,
+												   param_names[P_ELEM_COUNT_HIST]);
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_DECHIST);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(basetypcache->eq_opr);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(typcoll);
+
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+		k++;
+	}
+
+	/*
+	 * RANGE_BOUNDS_HIST => STATISTIC_KIND_BOUNDS_HISTOGRAM
+	 *
+	 * 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 (!PG_ARGISNULL(P_RANGE_BOUNDS_HIST))
+	{
+		Datum		strvalue = PG_GETARG_DATUM(P_RANGE_BOUNDS_HIST);
+		Datum		stavalues = cast_stavalues(&finfo, strvalue,
+											   typcache->type_id, typmod);
+
+		validate_bounds_histogram(stavalues, strvalue, typcache,
+								  param_names[P_RANGE_BOUNDS_HIST]);
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_BOUNDS_HISTOGRAM);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+		k++;
+	}
+
+	/*
+	 * P_RANGE_LENGTH_HIST && P_RANGE_EMPTY_FRAC =>
+	 * STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM
+	 */
+	if (has_rl_hist)
+	{
+		Datum		strvalue = PG_GETARG_DATUM(P_RANGE_LENGTH_HIST);
+
+		/* The anyarray is always a float8[] for this stakind */
+		Datum		stavalues = cast_stavalues(&finfo, strvalue, FLOAT8OID, 0);
+		Datum		elem = PG_GETARG_DATUM(P_RANGE_EMPTY_FRAC);
+		Datum		elems[] = {elem};
+		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+
+		validate_range_length_histogram(stavalues,
+										DatumGetFloat4(elem),
+										param_names[P_RANGE_LENGTH_HIST],
+										param_names[P_RANGE_EMPTY_FRAC]);
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] =
+			Int16GetDatum(STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(Float8LessOperator);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = PointerGetDatum(arry);
+		values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+		k++;
+	}
+
+	/* fill in all remaining slots */
+	for (; k < STATISTIC_NUM_SLOTS; k++)
+	{
+		values[Anum_pg_statistic_stakind1 - 1 + k] = Int16GetDatum(0);
+		values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = ObjectIdGetDatum(InvalidOid);
+		nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+	}
+
+	update_pg_statistic(values, nulls);
+
+	relation_close(rel, NoLock);
+	PG_RETURN_VOID();
+}
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..482f1eb2a3
--- /dev/null
+++ b/src/test/regress/expected/stats_export_import.out
@@ -0,0 +1,942 @@
+CREATE SCHEMA stats_export_import;
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        0 |        -1 |             0
+(1 row)
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, 999, 3.6::real, 15000);
+ pg_set_relation_stats 
+-----------------------
+ 
+(1 row)
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+      999 |       3.6 |         15000
+(1 row)
+
+-- error: relpages null
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, NULL, 3.6::real, 15000);
+ERROR:  relpages cannot be NULL
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+      999 |       3.6 |         15000
+(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:  could not open relation with OID 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_export_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_export_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_export_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_export_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_export_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_export_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 *
+FROM pg_stats
+WHERE schemaname = 'stats_export_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_export_import | test      | id      | f         |       0.1 |         2 |        0.3 |                  |                   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- error: null_frac < 0
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => -0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  null_frac -0.100000 is out of range 0.0 to 1.0
+-- error: null_frac > 1
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 1.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+ERROR:  null_frac 1.100000 is out of range 0.0 to 1.0
+-- error: avg_width < 0
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => -1::integer,
+    n_distinct => 0.3::real);
+ERROR:  avg_width -1 must be >= 0
+-- error: n_distinct < -1
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -1.1::real);
+ERROR:  n_distinct -1.100000 must be >= -1.0
+-- error: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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 => NULL::text,
+    most_common_freqs => '{0.1,0.2,0.3}'::real[]
+    );
+ERROR:  most_common_vals cannot be NULL when most_common_freqs is NOT NULL
+-- error: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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,
+    most_common_freqs => NULL::real[]
+    );
+ERROR:  most_common_freqs cannot be NULL when most_common_vals is NOT NULL
+-- error: mcv / mcf length mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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.2,0.1}'::real[]
+    );
+ERROR:  most_common_freqs has 2 elements, but most_common_vals has 3 elements, but they must be equal
+-- error: mcf sum bad
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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.6,0.5,0.3}'::real[]
+    );
+ERROR:  The sum of elements in most_common_freqs must not exceed 1.10 but is 1.400000
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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_export_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_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 | {2,1,3}          | {0.3,0.25,0.05}   |                  |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- error: histogram elements null value
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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
+-- error: histogram elements must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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,20,3,4}'::text
+    );
+ERROR:  histogram_bounds values must be in ascending order {1,20,3,4}
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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_export_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_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   | {1,2,3,4}        |             |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- error: correlation low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => -1.1::real);
+ERROR:  correlation -1.100000 is out of range -1.0 to 1.0
+-- error: correlation high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 1.1::real);
+ERROR:  correlation 1.100000 is out of range -1.0 to 1.0
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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_export_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_export_import | test      | id      | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |         0.5 |                   |                        |                      |                        |                  | 
+(1 row)
+
+-- error: scalars can't have mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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:  Relation test attname id is a scalar type, cannot have stats of type most_common_elems
+-- error: mcelem / mcelem null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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,
+    most_common_elem_freqs => NULL::real[]
+    );
+ERROR:  most_common_elem_freqs cannot be NULL when most_common_elems is NOT NULL
+-- error: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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 => NULL::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
+    );
+ERROR:  most_common_elems cannot be NULL when most_common_elem_freqs is NOT NULL
+-- error: mcelem / mcelem length mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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}'::real[]
+    );
+ERROR:  most_common_elem_freqs has 2 elements, but must have between 4 and 5 because most_common_elems has 2 elements
+-- error: mcelem freq element out of bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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.1,0.2,0.3,0.0}'::real[]
+    );
+ERROR:  most_common_elem_freqs frequency 0.100000 is out of range 0.200000 to 0.300000
+-- error: mcelem freq low-high mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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.4,0.3,0.0}'::real[]
+    );
+ERROR:  most_common_elem_freqs frequency low bound 0.400000 cannot be greater than high bound 0.300000
+-- error: mcelem freq null pct invalid
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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.0001}'::real[]
+    );
+ERROR:  most_common_elem_freqs null frequency -0.000100 is out of range 0.0 to 1.0
+-- error: mcelem freq bad low bound low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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.15,0.3,0.1}'::real[]
+    );
+ERROR:  most_common_elem_freqs minimum frequency -0.150000 is out of range 0.0 to 1.0
+-- error: mcelem freq bad low bound high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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,1.5,0.3,0.1}'::real[]
+    );
+ERROR:  most_common_elem_freqs minimum frequency 1.500000 is out of range 0.0 to 1.0
+-- error: mcelem freq bad high bound low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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.4,-0.3,0.1}'::real[]
+    );
+ERROR:  most_common_elem_freqs maximum frequency -0.300000 is out of range 0.0 to 1.0
+-- error: mcelem freq bad high bound high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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.4,3.0,0.1}'::real[]
+    );
+ERROR:  most_common_elem_freqs maximum frequency 3.000000 is out of range 0.0 to 1.0
+-- error mcelem values must be monotonically increasing
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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 => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.3,0.2,0.3,0.0}'::real[]
+    );
+ERROR:  most_common_elems values must be unqiue and in ascending order {three,one}
+-- error mcelem values must be unique
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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,three}'::text,
+    most_common_elem_freqs => '{0.3,0.4,0.4,0.2,0.5,0.0}'::real[]
+    );
+ERROR:  most_common_elems values must be unqiue and in ascending order {one,three,three}
+-- ok: mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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_export_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_export_import | test      | tags    | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             | {one,three}       | {0.3,0.2,0.2,0.3,0}    |                      |                        |                  | 
+(1 row)
+
+-- error: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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:  Relation test attname id is a scalar type, cannot have stats of type elem_count_histogram
+-- error: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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
+-- error: elem_count_histogram must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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,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}'::real[]
+    );
+ERROR:  elem_count_histogram array values must be in ascending order, but 1.000000 > 0.000000
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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_export_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_export_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)
+
+-- error: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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:  Relation test attname id is not a range type, cannot have stats of type range_length_histogram
+-- error: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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 cannot be NULL when range_length_histogram is NOT NULL
+-- error: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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 cannot be NULL when range_empty_frac is NOT NULL
+-- error: range_empty_frac low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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
+    );
+ERROR:  range_empty_frac -0.500000 is out of range 0.0 to 1.0
+-- error: range_empty_frac high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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 => 1.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+ERROR:  range_empty_frac 1.500000 is out of range 0.0 to 1.0
+-- error: range_length_histogram not ascending
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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,Infinity,499}'::text
+    );
+ERROR:  range_length_histogram array values must be in ascending order, but Infinity > 499.000000
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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_export_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_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      | {399,499,Infinity}     |              0.5 | 
+(1 row)
+
+-- error: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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:  Relation test attname id is not a range type, cannot have stats of type range_bounds_histogram
+-- error: range_bound_hist low bounds must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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)","[-2,4)","[1,4)","[1,100)"}'::text
+    );
+ERROR:  range_bounds_histogram array lower bounds must be in ascending order
+-- error: range_bound_hist high bounds must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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,11)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+ERROR:  range_bounds_histogram array upper bounds must be in ascending order
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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_export_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_export_import | test      | arange  | f         |       0.5 |         2 |       -0.1 |                  |                   |                  |             |                   |                        |                      |                        |                  | {"[-1,1)","[0,4)","[1,4)","[1,100)"}
+(1 row)
+
+-- error: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[],
+    histogram_bounds => '{1,2,3,4}'::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:  imported statistics must have a maximum of 5 slots but 6 given
+--
+-- 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_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_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_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_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_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 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(1 row)
+
+ pg_set_attribute_stats 
+------------------------
+ 
+(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         |         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_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 5ac6e871f5..a7a4dfd411 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,8 @@ test: strings md5 numerology point lseg line box path polygon circle date time t
 # geometry depends on point, lseg, line, box, path, polygon, circle
 # horology depends on date, time, timetz, timestamp, timestamptz, interval
 # ----------
-test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc stats_export_import
+
 
 # ----------
 # Load huge amounts of data
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..1a7d02a2c7
--- /dev/null
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -0,0 +1,836 @@
+CREATE SCHEMA stats_export_import;
+
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b real,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    arange int4range,
+    tags text[]
+);
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, 999, 3.6::real, 15000);
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+-- error: relpages null
+SELECT pg_set_relation_stats('stats_export_import.test'::regclass, NULL, 3.6::real, 15000);
+
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_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_export_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_export_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_export_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_export_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_export_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_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+SELECT *
+FROM pg_stats
+WHERE schemaname = 'stats_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- error: null_frac < 0
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => -0.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: null_frac > 1
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 1.1::real,
+    avg_width => 2::integer,
+    n_distinct => 0.3::real);
+
+-- error: avg_width < 0
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => -1::integer,
+    n_distinct => 0.3::real);
+
+-- error: n_distinct < -1
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -1.1::real);
+
+-- error: mcv / mcf null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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 => NULL::text,
+    most_common_freqs => '{0.1,0.2,0.3}'::real[]
+    );
+
+-- error: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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,
+    most_common_freqs => NULL::real[]
+    );
+
+-- error: mcv / mcf length mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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.2,0.1}'::real[]
+    );
+
+-- error: mcf sum bad
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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.6,0.5,0.3}'::real[]
+    );
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- error: histogram elements null value
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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 elements must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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,20,3,4}'::text
+    );
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- error: correlation low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => -1.1::real);
+
+-- error: correlation high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_import.test'::regclass,
+    attname => 'id'::name,
+    inherited => false::boolean,
+    null_frac => 0.5::real,
+    avg_width => 2::integer,
+    n_distinct => -0.1::real,
+    correlation => 1.1::real);
+
+-- ok: correlation
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'id';
+
+-- error: scalars can't have mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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: mcelem / mcelem null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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,
+    most_common_elem_freqs => NULL::real[]
+    );
+
+-- error: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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 => NULL::text,
+    most_common_elem_freqs => '{0.3,0.2,0.2,0.3}'::real[]
+    );
+
+-- error: mcelem / mcelem length mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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}'::real[]
+    );
+
+-- error: mcelem freq element out of bounds
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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.1,0.2,0.3,0.0}'::real[]
+    );
+
+-- error: mcelem freq low-high mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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.4,0.3,0.0}'::real[]
+    );
+
+-- error: mcelem freq null pct invalid
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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.0001}'::real[]
+    );
+
+-- error: mcelem freq bad low bound low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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.15,0.3,0.1}'::real[]
+    );
+
+-- error: mcelem freq bad low bound high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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,1.5,0.3,0.1}'::real[]
+    );
+
+-- error: mcelem freq bad high bound low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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.4,-0.3,0.1}'::real[]
+    );
+
+-- error: mcelem freq bad high bound high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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.4,3.0,0.1}'::real[]
+    );
+
+-- error mcelem values must be monotonically increasing
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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 => '{three,one}'::text,
+    most_common_elem_freqs => '{0.3,0.3,0.2,0.3,0.0}'::real[]
+    );
+-- error mcelem values must be unique
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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,three}'::text,
+    most_common_elem_freqs => '{0.3,0.4,0.4,0.2,0.5,0.0}'::real[]
+    );
+
+-- ok: mcelem 
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- error: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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: elem_count_histogram null element
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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 must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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,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}'::real[]
+    );
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'tags';
+
+-- error: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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 range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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_empty_frac low
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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
+    );
+-- error: range_empty_frac high
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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 => 1.5::real,
+    range_length_histogram => '{399,499,Infinity}'::text
+    );
+-- error: range_length_histogram not ascending
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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,Infinity,499}'::text
+    );
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- error: scalars can't have range stats
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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: range_bound_hist low bounds must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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)","[-2,4)","[1,4)","[1,100)"}'::text
+    );
+-- error: range_bound_hist high bounds must be in order
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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,11)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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_export_import'
+AND tablename = 'test'
+AND inherited = false
+AND attname = 'arange';
+
+-- error: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_set_attribute_stats(
+    relation => 'stats_export_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,1,3}'::text,
+    most_common_freqs => '{0.3,0.25,0.05}'::real[],
+    histogram_bounds => '{1,2,3,4}'::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_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_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_export_import.complex_type,  int4range(1,4), array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_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_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
+
+SELECT
+    format('SELECT pg_catalog.pg_set_attribute_stats( '
+            || 'relation => %L::regclass::oid, attname => %L::name, '
+            || 'inherited => %L::boolean, null_frac => %L::real, '
+            || 'avg_width => %L::integer, n_distinct => %L::real, '
+            || 'most_common_vals => %L::text, '
+            || 'most_common_freqs => %L::real[], '
+            || 'histogram_bounds => %L::text, '
+            || 'correlation => %L::real, '
+            || 'most_common_elems => %L::text, '
+            || 'most_common_elem_freqs => %L::real[], '
+            || 'elem_count_histogram => %L::real[], '
+            || 'range_length_histogram => %L::text, '
+            || 'range_empty_frac => %L::real, '
+            || 'range_bounds_histogram => %L::text) ',
+        'stats_export_import.' || s.tablename || '_clone', s.attname,
+        s.inherited, s.null_frac,
+        s.avg_width, s.n_distinct,
+        s.most_common_vals, s.most_common_freqs, s.histogram_bounds,
+        s.correlation, s.most_common_elems, s.most_common_elem_freqs,
+        s.elem_count_histogram, s.range_length_histogram,
+        s.range_empty_frac, s.range_bounds_histogram)
+FROM pg_catalog.pg_stats AS s
+WHERE s.schemaname = 'stats_export_import'
+AND s.tablename 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 93b0bc2bc6..153d0dc6ac 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29123,6 +29123,128 @@ 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>relpages</parameter> <type>integer</type>,
+         <parameter>reltuples</parameter> <type>real</type>,
+         <parameter>relallvisible</parameter> <type>integer</type> )
+        <returnvalue>void</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>
+        This function mimics the behavior of <command>ANALYZE</command> in its
+        effects on the values in <structname>pg_class</structname>, except that
+        the values are supplied as parameters rather than derived from table
+        sampling.
+       </para>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       <para>
+        The parameters supplied must all be NOT NULL. The value of
+        <structfield>relpages</structfield> must not be less than 0.  The
+        value of <structfield>reltuples</structfield> must not be less than
+        -1.0.  The value of <structfield>relallvisible</structfield> must not
+        be less than 0.
+       </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>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>
+         [, <parameter>most_common_vals</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_freqs</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>histogram_bounds</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>correlation</parameter> <type>real</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_elems</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>most_common_elem_freqs</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>elem_count_histogram</parameter> <type>real[]</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_length_histogram</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_empty_frac</parameter> <type>real</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ]
+         [, <parameter>range_bounds_histogram</parameter> <type>text</type>
+         <literal>DEFAULT</literal> <literal>NULL</literal> ] )
+        <returnvalue>void</returnvalue>
+       </para>
+       <para>
+        Replaces the <structname>pg_statistic</structname> row for the
+        <structname>pg_attribute</structname> row specified by
+        <parameter>relation</parameter>, <parameter>attname</parameter>
+        and <parameter>inherited</parameter>. The remaining parameters
+        all correspond to attributes of the same name found in
+        <link linkend="view-pg-stats"><structname>pg_stats</structname></link>,
+        and the values supplied in the parameter must meet the requirements of
+        the corresponding attribute.
+       </para>
+       <para>
+        This function mimics the behavior of <command>ANALYZE</command> in its
+        effects on the values in <structname>pg_statistic</structname>, except
+        that the values are supplied as parameters rather than derived from
+        table sampling.
+       </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 <command>ANALYZE</command>, usually via
+        <command>autovacuum</command> 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>
+       <para>
+        The caller must either be the owner of the relation, or have superuser
+        privileges.
+       </para>
+       </entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
-- 
2.44.0

