v23-0001-Create-statistics-set-and-restore-functions.patch

text/x-patch

Filename: v23-0001-Create-statistics-set-and-restore-functions.patch
Type: text/x-patch
Part: 0
Message: Re: Statistics Import and Export

Patch

Same data as JSON: GET /api/v1/attachments/:id/patch the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes. API reference →
Format: format-patch
Series: patch v23-0001
Subject: Create statistics set and restore functions.
File+
doc/src/sgml/func.sgml 399 0
src/backend/statistics/Makefile 2 1
src/backend/statistics/meson.build 1 0
src/backend/statistics/statistics.c 1673 0
src/include/catalog/pg_proc.dat 33 0
src/include/statistics/statistics.h 4 0
src/test/regress/expected/stats_export_import.out 870 0
src/test/regress/parallel_schedule 1 1
src/test/regress/sql/stats_export_import.sql 677 0
From 7e1443d12757851cd58cb5f116c30ddfda0b8ffa Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huinker@gmail.com>
Date: Wed, 17 Jul 2024 23:23:23 -0400
Subject: [PATCH v23 1/3] Create statistics set and restore functions.

The function pg_set_relation_stats is used to tweak statistics on any
relation that the user owns. Each parameter of pg_set_relation_stats
corresponds to a statistics attribute in pg_class.

The function pg_set_attribute_stats is used to tweak statistics on any
attribute of a relation that the user owns. Each parameter of
pg_set_attribute_stats corresponds to a statistics attribute of
the pg_stats view.

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 supplied have
to make sense in the context of the new relation. For example, it is not
possible to assign most_common_elems statistics to a scalar attribute.

These functions 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.

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.

The functions pg_restore_relation_stats and pg_restore_attribute_stats
are variadic variants of the pg_set_relation_stats and
pg_set_attribute_stats, respectively. The variadic parametrs are in
name-value pairs, with each name matching a parameter name in the
corresponding -set function. The paired value parameter must be of the
appropriate type for the corresponding parameter in the -set function,
with the exception that ANYARRAY values are passed as TEXT. The
intention of these functions is to be used in pg_dump/pg_restore and
pg_upgrade to allow the user to avoid having to run vacuumdb
--analyze-in-stages after an upgrade or restore.
---
 src/include/catalog/pg_proc.dat               |   33 +
 src/include/statistics/statistics.h           |    4 +
 src/backend/statistics/Makefile               |    3 +-
 src/backend/statistics/meson.build            |    1 +
 src/backend/statistics/statistics.c           | 1673 +++++++++++++++++
 .../regress/expected/stats_export_import.out  |  870 +++++++++
 src/test/regress/parallel_schedule            |    2 +-
 src/test/regress/sql/stats_export_import.sql  |  677 +++++++
 doc/src/sgml/func.sgml                        |  399 ++++
 9 files changed, 3660 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 73d9cf8582..42ee44bce6 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12208,4 +12208,37 @@
   proargnames => '{summarized_tli,summarized_lsn,pending_lsn,summarizer_pid}',
   prosrc => 'pg_get_wal_summarizer_state' },
 
+# Statistics Import
+{ oid => '8048',
+  descr => 'set statistics on relation',
+  proname => 'pg_set_relation_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'oid int4 int4 float4 int4',
+  proargnames => '{relation,version,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 => 'bool',
+  proargtypes => 'oid name bool int4 float4 int4 float4 text _float4 text float4 text _float4 _float4 text float4 text',
+  proargnames => '{relation,attname,inherited,version,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' },
+{ oid => '8050',
+  descr => 'set statistics on relation',
+  proname => 'pg_restore_relation_stats', provariadic => 'any',
+  proisstrict => 'f', provolatile => 'v',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'regclass int4 any',
+  proargnames => '{relation,version,stats}',
+  proargmodes => '{i,i,v}',
+  prosrc => 'pg_restore_relation_stats' },
+{ oid => '8051',
+  descr => 'set statistics on attribute',
+  proname => 'pg_restore_attribute_stats', provariadic => 'any',
+  proisstrict => 'f', provolatile => 'v',
+  proparallel => 'u', prorettype => 'bool',
+  proargtypes => 'regclass name bool int4 any',
+  proargnames => '{relation,attname,inherited,version,stats}',
+  proargmodes => '{i,i,i,i,v}',
+  prosrc => 'pg_restore_attribute_stats' },
 ]
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 7f2bf18716..f0503d1c87 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,4 +127,8 @@ 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);
+extern Datum pg_restore_relation_stats(PG_FUNCTION_ARGS);
+extern Datum pg_restore_attribute_stats(PG_FUNCTION_ARGS);
 #endif							/* STATISTICS_H */
diff --git a/src/backend/statistics/Makefile b/src/backend/statistics/Makefile
index 89cf8c2797..e4f8ab7c4f 100644
--- a/src/backend/statistics/Makefile
+++ b/src/backend/statistics/Makefile
@@ -16,6 +16,7 @@ OBJS = \
 	dependencies.o \
 	extended_stats.o \
 	mcv.o \
-	mvdistinct.o
+	mvdistinct.o \
+	statistics.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/statistics/meson.build b/src/backend/statistics/meson.build
index 73b29a3d50..331e82c776 100644
--- a/src/backend/statistics/meson.build
+++ b/src/backend/statistics/meson.build
@@ -5,4 +5,5 @@ backend_sources += files(
   'extended_stats.c',
   'mcv.c',
   'mvdistinct.c',
+  'statistics.c',
 )
diff --git a/src/backend/statistics/statistics.c b/src/backend/statistics/statistics.c
new file mode 100644
index 0000000000..8c6b159514
--- /dev/null
+++ b/src/backend/statistics/statistics.c
@@ -0,0 +1,1673 @@
+/*-------------------------------------------------------------------------
+ * statistics.c
+ *
+ *	  POSTGRES statistics import
+ *
+ * Code supporting the direct importation of relation statistics, similar to
+ * what is done by the ANALYZE command.
+ *
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *       src/backend/statistics/statistics.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_collation.h"
+#include "catalog/pg_database.h"
+#include "catalog/pg_operator.h"
+#include "catalog/pg_type.h"
+#include "fmgr.h"
+#include "funcapi.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/elog.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"
+
+/*
+ * Names of parameters found in both pg_set_X_stats and pg_restore_X_stats
+ * functions.
+ */
+const char *relation_name = "relation";
+const char *relpages_name = "relpages";
+const char *reltuples_name = "reltuples";
+const char *relallvisible_name = "relallvisible";
+const char *attname_name = "attname";
+const char *inherited_name = "inherited";
+const char *null_frac_name = "null_frac";
+const char *avg_width_name = "avg_width";
+const char *n_distinct_name = "n_distinct";
+const char *mc_vals_name = "most_common_vals";
+const char *mc_freqs_name = "most_common_freqs";
+const char *histogram_bounds_name = "histogram_bounds";
+const char *correlation_name = "correlation";
+const char *mc_elems_name = "most_common_elems";
+const char *mc_elem_freqs_name = "most_common_elem_freqs";
+const char *elem_count_hist_name = "elem_count_histogram";
+const char *range_length_hist_name = "range_length_histogram";
+const char *range_empty_frac_name = "range_empty_frac";
+const char *range_bounds_hist_name = "range_bounds_histogram";
+
+/*
+ * A role has privileges to set statistics on 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(Relation rel)
+{
+	Oid			relid = RelationGetRelid(rel);
+	Form_pg_class reltuple = rel->rd_rel;
+
+	return ((object_ownercheck(DatabaseRelationId, MyDatabaseId, GetUserId())
+			 && !reltuple->relisshared) ||
+			pg_class_aclcheck(relid, GetUserId(), ACL_MAINTAIN) == ACLCHECK_OK);
+}
+
+/*
+ * 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)
+{
+
+	Datum		relation_datum = PG_GETARG_DATUM(0);
+	bool		relation_isnull = PG_ARGISNULL(0);
+	Oid			relation;
+
+	Datum		version_datum = PG_GETARG_DATUM(1);
+	bool		version_isnull = PG_ARGISNULL(1);
+	int			version;
+
+	Datum		relpages_datum = PG_GETARG_DATUM(2);
+	bool		relpages_isnull = PG_ARGISNULL(2);
+	int			relpages;
+
+	Datum		reltuples_datum = PG_GETARG_DATUM(3);
+	bool		reltuples_isnull = PG_ARGISNULL(3);
+	float4		reltuples;
+
+	Datum		relallvisible_datum = PG_GETARG_DATUM(4);
+	bool		relallvisible_isnull = PG_ARGISNULL(4);
+	int			relallvisible;
+
+	Relation	rel;
+	HeapTuple	ctup;
+	Form_pg_class pgcform;
+
+	/* If we don't know what relation we're modifying, give up */
+	if (relation_isnull)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Parameter %s cannot be null", relation_name)));
+		PG_RETURN_BOOL(false);
+	}
+	else
+		relation = DatumGetObjectId(relation_datum);
+
+	/* NULL version means assume current server version */
+	if (version_isnull)
+		version = PG_VERSION_NUM;
+	else
+	{
+		version = DatumGetInt32(version_datum);
+		if (version < 90200)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Cannot export statistics prior to version 9.2")));
+			PG_RETURN_BOOL(false);
+		}
+	}
+
+	if (relpages_isnull)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", relpages_name)));
+		PG_RETURN_BOOL(false);
+	}
+	else
+	{
+		relpages = DatumGetInt32(relpages_datum);
+		if (relpages < -1)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be < -1", relpages_name)));
+			PG_RETURN_BOOL(false);
+		}
+	}
+
+	if (reltuples_isnull)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", reltuples_name)));
+		PG_RETURN_BOOL(false);
+	}
+	else
+	{
+		reltuples = DatumGetFloat4(reltuples_datum);
+		if (reltuples < -1.0)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be < -1.0", reltuples_name)));
+			PG_RETURN_BOOL(false);
+		}
+	}
+
+	if (relallvisible_isnull)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", relallvisible_name)));
+		PG_RETURN_BOOL(false);
+	}
+	else
+	{
+		relallvisible = DatumGetInt32(relallvisible_datum);
+		if (relallvisible < -1)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s cannot be < -1", relallvisible_name)));
+			PG_RETURN_BOOL(false);
+		}
+	}
+
+	/*
+	 * 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, relation_datum);
+	if (!HeapTupleIsValid(ctup))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_OBJECT_IN_USE),
+				 errmsg("pg_class entry for relid %u not found", relation)));
+		PG_RETURN_BOOL(false);
+	}
+
+	pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+	if (!can_modify_relation(rel))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(rel))));
+		PG_RETURN_BOOL(false);
+	}
+
+	/* Only update pg_class if there is a meaningful change */
+	if ((pgcform->reltuples != reltuples)
+		|| (pgcform->relpages != relpages)
+		|| (pgcform->relallvisible != relallvisible))
+	{
+		pgcform->relpages = relpages;
+		pgcform->reltuples = reltuples;
+		pgcform->relallvisible = relallvisible;
+
+		heap_inplace_update(rel, ctup);
+	}
+
+	table_close(rel, NoLock);
+
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * Restore 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.
+ *
+ * The actual modification of stats happesn in a call to pg_set_relation_stats(),
+ * which has a named parameters for each statistic type. This function serves
+ * as a way to allow stats import calls written for a previous version to work
+ * on the current version, regardless of what parameters were introduced or
+ * removed in the time in between.
+ *
+ */
+Datum
+pg_restore_relation_stats(PG_FUNCTION_ARGS)
+{
+	Datum		relation_datum = PG_GETARG_DATUM(0);
+	bool		relation_isnull = PG_ARGISNULL(0);
+
+	Datum		version_datum = PG_GETARG_DATUM(1);
+	bool		version_isnull = PG_ARGISNULL(1);
+
+	/* Flags to indicate that a datum has been found once already */
+	bool		relpages_set = false;
+	bool		reltuples_set = false;
+	bool		relallvisible_set = false;
+
+	/* build argument values to build the object */
+	Datum	   *args;
+	bool	   *argnulls;
+	Oid		   *types;
+	int			nargs;
+
+	LOCAL_FCINFO(set_fcinfo, 5);
+	Datum		result;
+
+	nargs = extract_variadic_args(fcinfo, 2, true, &args, &types, &argnulls);
+
+	/* if the pairs aren't pairs, something is malformed */
+	if (nargs % 2 == 1)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("stats parameters must be in name-value pairs")));
+		PG_RETURN_BOOL(false);
+	}
+
+	InitFunctionCallInfoData(*set_fcinfo, NULL, 5, InvalidOid, NULL, NULL);
+
+	set_fcinfo->args[0].value = relation_datum;
+	set_fcinfo->args[0].isnull = relation_isnull;
+	set_fcinfo->args[1].value = version_datum;
+	set_fcinfo->args[1].isnull = version_isnull;
+
+	/* Assume an argument is NULL unless matching pair found */
+	for (int i = 2; i < 5; i++)
+	{
+		set_fcinfo->args[i].value = (Datum) 0;
+		set_fcinfo->args[i].isnull = true;
+	}
+
+	/* loop through args, matching params to their arg indexes */
+	for (int i = 0; i < nargs; i += 2)
+	{
+		char	   *statname;
+		int			argidx = i + 1;
+
+		if (types[i] != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("stat names must be of type text")));
+			continue;
+		}
+		statname = TextDatumGetCString(args[i]);
+
+		/*
+		 * Match each named parameter to the index of the value that follows
+		 * it. Skip past any duplicates. Verify that value parameter is of the
+		 * correct type.
+		 */
+		if (strcmp(statname, relpages_name) == 0)
+		{
+			if (relpages_set)
+			{
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("stat name %s already used, subsequent values ignored",
+								relpages_name)));
+				continue;
+			}
+			else if (types[argidx] != INT4OID)
+			{
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be of type integer",
+								relpages_name)));
+				PG_RETURN_BOOL(false);
+			}
+			else
+			{
+				relpages_set = true;
+				set_fcinfo->args[2].value = args[argidx];
+				set_fcinfo->args[2].isnull = argnulls[argidx];
+			}
+		}
+		else if (strcmp(statname, reltuples_name) == 0)
+		{
+			if (reltuples_set)
+			{
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("stat name %s already used, subsequent values ignored",
+								reltuples_name)));
+				continue;
+			}
+			else if (types[argidx] != FLOAT4OID)
+			{
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be of type real", reltuples_name)));
+				PG_RETURN_BOOL(false);
+			}
+			else
+			{
+				reltuples_set = true;
+				set_fcinfo->args[3].value = args[argidx];
+				set_fcinfo->args[3].isnull = argnulls[argidx];
+			}
+		}
+		else if (strcmp(statname, relallvisible_name) == 0)
+		{
+			if (relallvisible_set)
+			{
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("stat name %s already used, subsequent values ignored",
+								relallvisible_name)));
+				continue;
+			}
+			else if (types[argidx] != INT4OID)
+			{
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be of type integer",
+								relallvisible_name)));
+				PG_RETURN_BOOL(false);
+			}
+			else
+			{
+				relallvisible_set = true;
+				set_fcinfo->args[4].value = args[argidx];
+				set_fcinfo->args[4].isnull = argnulls[argidx];
+			}
+		}
+	}
+
+	result = (*pg_set_relation_stats) (set_fcinfo);
+
+	PG_RETURN_DATUM(result);
+}
+
+/*
+ * 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;
+}
+
+/*
+ * If this relation is an index and that index has expressions in it, and
+ * the attnum specified is known to be an expression, then we must walk
+ * the list attributes up to the specified attnum to get the right
+ * expression.
+ */
+static Node *
+get_attr_expr(Relation rel, int attnum)
+{
+	if ((rel->rd_rel->relkind == RELKIND_INDEX
+		 || (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+		&& (rel->rd_indexprs != NIL)
+		&& (rel->rd_index->indkey.values[attnum - 1] == 0))
+	{
+		ListCell   *indexpr_item = list_head(rel->rd_indexprs);
+
+		for (int i = 0; i < attnum - 1; i++)
+			if (rel->rd_index->indkey.values[i] == 0)
+				indexpr_item = lnext(rel->rd_indexprs, indexpr_item);
+
+		if (indexpr_item == NULL)	/* shouldn't happen */
+			elog(ERROR, "too few entries in indexprs list");
+
+		return (Node *) lfirst(indexpr_item);
+	}
+	return NULL;
+}
+
+/*
+ * Fetch datatype information, this is needed to derive the proper staopN
+ * and stacollN values.
+ *
+ */
+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;
+	Node	   *expr;
+
+	atup = SearchSysCache2(ATTNAME, ObjectIdGetDatum(relid),
+						   NameGetDatum(attname));
+
+	/* Attribute not found */
+	if (!HeapTupleIsValid(atup))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s has no attname %s",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+		return NULL;
+	}
+
+	attr = (Form_pg_attribute) GETSTRUCT(atup);
+	if (attr->attisdropped)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("Relation %s attname %s is dropped",
+						RelationGetRelationName(rel),
+						NameStr(*attname))));
+		return NULL;
+	}
+	*attnum = attr->attnum;
+
+	expr = get_attr_expr(rel, attr->attnum);
+
+	if (expr == NULL)
+	{
+		/* regular attribute */
+		typid = attr->atttypid;
+		*typmod = attr->atttypmod;
+		*typcoll = attr->attcollation;
+	}
+	else
+	{
+		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);
+	}
+	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);
+}
+
+/*
+ * Perform the cast of a known TextDatum into the type specified.
+ *
+ * If no errors are found, ok is set to true. Otherwise, set ok
+ * to false, capture the error found, and re-throw at warning level.
+ */
+static Datum
+cast_stavalues(FmgrInfo *flinfo, Datum d, Oid typid, int32 typmod, bool *ok)
+{
+	LOCAL_FCINFO(fcinfo, 8);
+	char	   *s;
+	Datum		result;
+	ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+	escontext.details_wanted = true;
+
+	s = TextDatumGetCString(d);
+
+	InitFunctionCallInfoData(*fcinfo, flinfo, 3, InvalidOid,
+							 (Node *) &escontext, NULL);
+
+	fcinfo->args[0].value = CStringGetDatum(s);
+	fcinfo->args[0].isnull = false;
+	fcinfo->args[1].value = ObjectIdGetDatum(typid);
+	fcinfo->args[1].isnull = false;
+	fcinfo->args[2].value = Int32GetDatum(typmod);
+	fcinfo->args[2].isnull = false;
+
+	result = FunctionCallInvoke(fcinfo);
+
+	if (SOFT_ERROR_OCCURRED(&escontext))
+	{
+		escontext.error_data->elevel = WARNING;
+		ThrowErrorData(escontext.error_data);
+		*ok = false;
+	}
+	else
+		*ok = true;
+
+	pfree(s);
+
+	return result;
+}
+
+
+/*
+ * Check array for any NULLs, and optionally for one-dimensionality.
+ *
+ * Report any failures as warnings.
+ */
+static bool
+array_check(Datum datum, int one_dim, const char *statname)
+{
+	ArrayType  *arr = DatumGetArrayTypeP(datum);
+	int16		elmlen;
+	char		elmalign;
+	bool		elembyval;
+	Datum	   *values;
+	bool	   *nulls;
+	int			nelems;
+
+	if (one_dim && (arr->ndim != 1))
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be a multidimensional array", statname)));
+		return false;
+	}
+
+	get_typlenbyvalalign(ARR_ELEMTYPE(arr), &elmlen, &elembyval, &elmalign);
+
+	deconstruct_array(arr, ARR_ELEMTYPE(arr), elmlen, elembyval, elmalign,
+					  &values, &nulls, &nelems);
+
+	for (int i = 0; i < nelems; i++)
+		if (nulls[i])
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("%s array cannot contain NULL values", statname)));
+			return false;
+		}
+	return true;
+}
+
+/*
+ * 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)
+{
+	Datum		relation_datum = PG_GETARG_DATUM(0);
+	bool		relation_isnull = PG_ARGISNULL(0);
+	Oid			relation = PG_GETARG_OID(0);
+
+	Name		attname = PG_GETARG_NAME(1);
+	bool		attname_isnull = PG_ARGISNULL(1);
+
+	Datum		inherited_datum = PG_GETARG_DATUM(2);
+	bool		inherited_isnull = PG_ARGISNULL(2);
+	/* TODO
+	bool		inherited = PG_GETARG_BOOL(2);
+	*/
+
+	int			version = PG_GETARG_INT32(3);
+	int			version_isnull = PG_ARGISNULL(3);
+
+	Datum		null_frac_datum = PG_GETARG_DATUM(4);
+	bool		null_frac_isnull = PG_ARGISNULL(4);
+
+	Datum		avg_width_datum = PG_GETARG_DATUM(5);
+	bool		avg_width_isnull = PG_ARGISNULL(5);
+
+	Datum		n_distinct_datum = PG_GETARG_DATUM(6);
+	bool		n_distinct_isnull = PG_ARGISNULL(6);
+
+	Datum		mc_vals_datum = PG_GETARG_DATUM(7);
+	bool		mc_vals_isnull = PG_ARGISNULL(7);
+
+	Datum		mc_freqs_datum = PG_GETARG_DATUM(8);
+	bool		mc_freqs_isnull = PG_ARGISNULL(8);
+
+	Datum		histogram_bounds_datum = PG_GETARG_DATUM(9);
+	bool		histogram_bounds_isnull = PG_ARGISNULL(9);
+
+	Datum		correlation_datum = PG_GETARG_DATUM(10);
+	bool		correlation_isnull = PG_ARGISNULL(10);
+
+	Datum		mc_elems_datum = PG_GETARG_DATUM(11);
+	bool		mc_elems_isnull = PG_ARGISNULL(11);
+
+	Datum		mc_elem_freqs_datum = PG_GETARG_DATUM(12);
+	bool		mc_elem_freqs_isnull = PG_ARGISNULL(12);
+
+	Datum		elem_count_hist_datum = PG_GETARG_DATUM(13);
+	bool		elem_count_hist_isnull = PG_ARGISNULL(13);
+
+	Datum		range_length_hist_datum = PG_GETARG_DATUM(14);
+	bool		range_length_hist_isnull = PG_ARGISNULL(14);
+
+	Datum		range_empty_frac_datum = PG_GETARG_DATUM(15);
+	bool		range_empty_frac_isnull = PG_ARGISNULL(15);
+
+	Datum		range_bounds_hist_datum = PG_GETARG_DATUM(16);
+	bool		range_bounds_hist_isnull = PG_ARGISNULL(16);
+
+	Relation	rel;
+
+	TypeCacheEntry *typcache;
+	TypeCacheEntry *elemtypcache = NULL;
+
+	int16		attnum;
+	int32		typmod;
+	Oid			typcoll;
+
+	FmgrInfo	finfo;
+
+	int			stakind_count;
+
+	/* known-used */
+	bool		has_mcv = false;
+	bool		has_mc_elems = false;
+	bool		has_rl_hist = false;
+
+	int			k = 0;
+
+	Datum		values[Natts_pg_statistic];
+	bool		nulls[Natts_pg_statistic];
+
+	/* initialize output tuple */
+	for (int i = 0; i < Natts_pg_statistic; i++)
+	{
+		values[i] = (Datum) 0;
+		nulls[i] = false;
+	}
+
+	/*
+	 * Some parameters are "required" in that nothing can happen if any of
+	 * them are NULL.
+	 */
+	if (relation_isnull)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", relation_name)));
+		PG_RETURN_BOOL(false);
+	}
+
+	if (attname_isnull)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", attname_name)));
+		PG_RETURN_BOOL(false);
+	}
+
+	if (inherited_isnull)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", inherited_name)));
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * NULL version means assume current server version
+	 */
+	if (version_isnull)
+		version = PG_VERSION_NUM;
+	else
+	{
+		if (version < 90200)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Cannot export statistics prior to version 9.2")));
+			PG_RETURN_BOOL(false);
+		}
+	}
+
+	if (null_frac_isnull)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", null_frac_name)));
+		PG_RETURN_BOOL(false);
+	}
+
+	if (avg_width_isnull)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", avg_width_name)));
+		PG_RETURN_BOOL(false);
+	}
+
+	if (n_distinct_isnull)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL", n_distinct_name)));
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Params most_common_vals and most_common_freqs are linked
+	 */
+	if ((!mc_vals_isnull) && (!mc_freqs_isnull))
+		has_mcv = true;
+	else if (mc_vals_isnull != mc_freqs_isnull)
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL when %s is NOT NULL",
+						mc_vals_isnull ? mc_vals_name : mc_freqs_name,
+						!mc_vals_isnull ? mc_vals_name : mc_freqs_name)));
+
+	/*
+	 * Params most_common_elems and most_common_elem_freqs are linked
+	 */
+	if ((!mc_elems_isnull) && (!mc_elem_freqs_isnull))
+		has_mc_elems = true;
+	else if (mc_elems_isnull != mc_elem_freqs_isnull)
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL when %s is NOT NULL",
+						mc_elems_isnull ? mc_elems_name : mc_elem_freqs_name,
+						!mc_elems_isnull ? mc_elems_name : mc_elem_freqs_name)));
+
+	/*
+	 * Params range_empty_frac and range_length_histogram are linked
+	 */
+	if ((!range_length_hist_isnull) && (!range_empty_frac_isnull))
+		has_rl_hist = true;
+	else if (range_length_hist_isnull != range_empty_frac_isnull)
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("%s cannot be NULL when %s is NOT NULL",
+						range_length_hist_isnull ?
+						range_length_hist_name : range_empty_frac_name,
+						!range_length_hist_isnull ?
+						range_length_hist_name : range_empty_frac_name)));
+
+	/*
+	 * If a caller specifies more stakind-stats than we have slots to store
+	 * them, reject them all
+	 */
+	stakind_count = (int) has_mcv + (int) has_mc_elems + (int) has_rl_hist +
+		(int) !histogram_bounds_isnull + (int) !correlation_isnull +
+		(int) !elem_count_hist_isnull + (int) !range_bounds_hist_isnull;
+
+	if (stakind_count > STATISTIC_NUM_SLOTS)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("imported statistics must have a maximum of %d slots "
+						"but %d given",
+						STATISTIC_NUM_SLOTS, stakind_count)));
+		PG_RETURN_BOOL(false);
+	}
+
+	rel = try_relation_open(relation, ShareUpdateExclusiveLock);
+
+	if (rel == NULL)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("Parameter relation OID %u is invalid", relation)));
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Many of the values that are set for a particular stakind are entirely
+	 * derived from the attribute itself, or it's expression.
+	 */
+	typcache = get_attr_stat_type(rel, attname, &attnum, &typmod, &typcoll);
+	if (typcache == NULL)
+	{
+		relation_close(rel, NoLock);
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("unexpected typecache error")));
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * Derive element type if we have stat 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 || !elem_count_hist_isnull)
+	{
+		Oid			elemtypid;
+
+		if (typcache->type_id == TSVECTOROID)
+		{
+			/*
+			 * tsvectors always have a text oid base type and default
+			 * collation
+			 */
+			elemtypid = TEXTOID;
+			typcoll = DEFAULT_COLLATION_OID;
+		}
+		else if (typcache->typtype == TYPTYPE_RANGE)
+			elemtypid = get_range_subtype(typcache->type_id);
+		else
+			elemtypid = get_base_element_type(typcache->type_id);
+
+		/* not finding a basetype means we already had it */
+		if (elemtypid == InvalidOid)
+			elemtypid = typcache->type_id;
+
+		/* The stats need the eq_opr, any validation would need the lt_opr */
+		elemtypcache = lookup_type_cache(elemtypid, TYPECACHE_EQ_OPR);
+		if (elemtypcache == NULL)
+		{
+			/* warn and ignore any stats that can't be fulfilled */
+			has_mc_elems = false;
+
+			if (!mc_elems_isnull)
+			{
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s cannot accept %s stats, ignored",
+								NameStr(*attname),
+								mc_elems_name)));
+				mc_elems_isnull = true;
+			}
+
+			if (!mc_elem_freqs_isnull)
+			{
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s cannot accept %s stats, ignored",
+								NameStr(*attname),
+								mc_elem_freqs_name)));
+				mc_elem_freqs_isnull = true;
+			}
+
+			if (!elem_count_hist_isnull)
+			{
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s cannot accept %s stats, ignored",
+								NameStr(*attname),
+								elem_count_hist_name)));
+				elem_count_hist_isnull = true;
+			}
+		}
+	}
+
+	/*
+	 * histogram_bounds and correlation must have a type < operator
+	 */
+	if (typcache->lt_opr == InvalidOid)
+	{
+		if (!histogram_bounds_isnull)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("Relation %s attname %s cannot "
+							"have stats of type %s, ignored.",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							histogram_bounds_name)));
+			histogram_bounds_isnull = true;
+		}
+
+		if (!correlation_isnull)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("Relation %s attname %s cannot "
+							"have stats of type %s, ignored.",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							correlation_name)));
+			correlation_isnull = true;
+		}
+	}
+
+	/*
+	 * Scalar types can't have most_common_elems, most_common_elem_freqs, or
+	 * element_count_histogram
+	 */
+	if (type_is_scalar(typcache->type_id))
+	{
+		/* warn and ignore any stats that can't be fulfilled */
+		has_mc_elems = false;
+
+		if (!mc_elems_isnull)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("Relation %s attname %s is a scalar type, "
+							"cannot have stats of type %s, ignored",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							mc_elems_name)));
+			mc_elems_isnull = true;
+		}
+
+		if (!mc_elem_freqs_isnull)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("Relation %s attname %s is a scalar type, "
+							"cannot have stats of type %s, ignored",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							mc_elem_freqs_name)));
+			mc_elem_freqs_isnull = true;
+		}
+
+		if (!elem_count_hist_isnull)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("Relation %s attname %s is a scalar type, "
+							"cannot have stats of type %s, ignored",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							elem_count_hist_name)));
+			elem_count_hist_isnull = true;
+		}
+	}
+
+	/* Only range types can have range stats */
+	if ((typcache->typtype != TYPTYPE_RANGE) &&
+		(typcache->typtype != TYPTYPE_MULTIRANGE))
+	{
+		/* warn and ignore any stats that can't be fulfilled */
+		has_rl_hist = false;
+
+		if (!range_length_hist_isnull)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("Relation %s attname %s is not a range type, "
+							"cannot have stats of type %s",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							range_length_hist_name)));
+			range_length_hist_isnull = true;
+		}
+
+		if (!range_empty_frac_isnull)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("Relation %s attname %s is not a range type, "
+							"cannot have stats of type %s",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							range_empty_frac_name)));
+			range_empty_frac_isnull = true;
+		}
+		if (!range_bounds_hist_isnull)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+					 errmsg("Relation %s attname %s is not a range type, "
+							"cannot have stats of type %s",
+							RelationGetRelationName(rel),
+							NameStr(*attname),
+							range_bounds_hist_name)));
+			range_bounds_hist_isnull = true;
+		}
+	}
+
+	if (!can_modify_relation(rel))
+	{
+		relation_close(rel, NoLock);
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("permission denied for relation %s",
+						RelationGetRelationName(rel))));
+		PG_RETURN_BOOL(false);
+	}
+
+	/*
+	 * TODO: this test may not be necessary
+	 *
+	 * On the one hand, we don't want to be able to inject stats that
+	 * cannot be removed. On the other hand, we may want to inject inherited
+	 * stats on a table that will later be inherited by another stats
+	 */
+	/*
+	if (inherited &&
+		(rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE) &&
+		(rel->rd_rel->relkind != RELKIND_PARTITIONED_INDEX) &&
+		(rel->rd_rel->relhassubclass == false))
+	{
+		relation_close(rel, NoLock);
+		ereport(WARNING,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("%s is not partitioned, cannot accepted inherited stats",
+						RelationGetRelationName(rel))));
+		PG_RETURN_BOOL(false);
+	}
+	*/
+
+	/* Populate pg_statistic tuple */
+	values[Anum_pg_statistic_starelid - 1] = relation_datum;
+	values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(attnum);
+	values[Anum_pg_statistic_stainherit - 1] = inherited_datum;
+	values[Anum_pg_statistic_stanullfrac - 1] = null_frac_datum;
+	values[Anum_pg_statistic_stawidth - 1] = avg_width_datum;
+	values[Anum_pg_statistic_stadistinct - 1] = n_distinct_datum;
+
+	fmgr_info(F_ARRAY_IN, &finfo);
+
+	/*
+	 * STATISTIC_KIND_MCV
+	 *
+	 * most_common_vals: ANYARRAY::text most_common_freqs: real[]
+	 */
+	if (has_mcv)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_MCV);
+		Datum		staop = ObjectIdGetDatum(typcache->eq_opr);
+		Datum		stacoll = ObjectIdGetDatum(typcoll);
+		bool		converted = false;
+		Datum		stanumbers = mc_freqs_datum;
+		Datum		stavalues = cast_stavalues(&finfo, mc_vals_datum,
+											   typcache->type_id, typmod,
+											   &converted);
+
+		if (converted &&
+			array_check(stavalues, false, mc_vals_name) &&
+			array_check(stanumbers, true, mc_freqs_name))
+		{
+			values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+			values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+			values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+			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++;
+		}
+	}
+
+
+	/*
+	 * STATISTIC_KIND_HISTOGRAM
+	 *
+	 * histogram_bounds: ANYARRAY::text
+	 */
+	if (!histogram_bounds_isnull)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_HISTOGRAM);
+		Datum		staop = ObjectIdGetDatum(typcache->lt_opr);
+		Datum		stacoll = ObjectIdGetDatum(typcoll);
+		Datum		stavalues;
+		bool		converted = false;
+
+		stavalues = cast_stavalues(&finfo, histogram_bounds_datum,
+								   typcache->type_id, typmod, &converted);
+
+		if (converted && array_check(stavalues, false, histogram_bounds_name))
+		{
+			values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+			values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+			values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+			nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+			values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+			k++;
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_CORRELATION
+	 *
+	 * correlation: real
+	 */
+	if (!correlation_isnull)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_CORRELATION);
+		Datum		staop = ObjectIdGetDatum(typcache->lt_opr);
+		Datum		stacoll = ObjectIdGetDatum(typcoll);
+		Datum		elems[] = {correlation_datum};
+		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+		Datum		stanumbers = PointerGetDatum(arry);
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+		values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+		k++;
+	}
+
+	/*
+	 * STATISTIC_KIND_MCELEM
+	 *
+	 * most_common_elem_freqs: real[]
+	 *
+	 * most_common_elems     : ANYARRAY::text
+	 */
+	if (has_mc_elems)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_MCELEM);
+		Datum		staop = ObjectIdGetDatum(elemtypcache->eq_opr);
+		Datum		stacoll = ObjectIdGetDatum(typcoll);
+		Datum		stanumbers = mc_elem_freqs_datum;
+		bool		converted = false;
+		Datum		stavalues;
+
+		stavalues = cast_stavalues(&finfo, mc_elems_datum,
+								   elemtypcache->type_id, typmod, &converted);
+
+		if (converted &&
+			array_check(stavalues, false, mc_elems_name) &&
+			array_check(stanumbers, true, mc_elem_freqs_name))
+		{
+			values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+			values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+			values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+			values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+			values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+			k++;
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_DECHIST
+	 *
+	 * elem_count_histogram:	real[]
+	 */
+	if (!elem_count_hist_isnull)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_DECHIST);
+		Datum		staop = ObjectIdGetDatum(elemtypcache->eq_opr);
+		Datum		stacoll = ObjectIdGetDatum(typcoll);
+		Datum		stanumbers = elem_count_hist_datum;
+
+		values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+		values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+		values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+		values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+		nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+
+		k++;
+	}
+
+	/*
+	 * STATISTIC_KIND_BOUNDS_HISTOGRAM
+	 *
+	 * range_bounds_histogram: ANYARRAY::text
+	 *
+	 * This stakind appears before STATISTIC_KIND_BOUNDS_HISTOGRAM even though
+	 * it is numerically greater, and all other stakinds appear in numerical
+	 * order. We duplicate this quirk to make before/after tests of
+	 * pg_statistic records easier.
+	 */
+	if (!range_bounds_hist_isnull)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_BOUNDS_HISTOGRAM);
+		Datum		staop = ObjectIdGetDatum(InvalidOid);
+		Datum		stacoll = ObjectIdGetDatum(InvalidOid);
+
+		bool		converted = false;
+		Datum		stavalues;
+
+		stavalues = cast_stavalues(&finfo, range_bounds_hist_datum,
+								   typcache->type_id, typmod, &converted);
+
+		if (converted &&
+			array_check(stavalues, false, "range_bounds_histogram"))
+		{
+			values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+			values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+			values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+			nulls[Anum_pg_statistic_stanumbers1 - 1 + k] = true;
+			values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+
+			k++;
+		}
+	}
+
+	/*
+	 * STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM
+	 *
+	 * range_empty_frac: real
+	 *
+	 * range_length_histogram:  double precision[]::text
+	 */
+	if (has_rl_hist)
+	{
+		Datum		stakind = Int16GetDatum(STATISTIC_KIND_RANGE_LENGTH_HISTOGRAM);
+		Datum		staop = ObjectIdGetDatum(Float8LessOperator);
+		Datum		stacoll = ObjectIdGetDatum(InvalidOid);
+
+		/* The anyarray is always a float8[] for this stakind */
+		Datum		elems[] = {range_empty_frac_datum};
+		ArrayType  *arry = construct_array_builtin(elems, 1, FLOAT4OID);
+		Datum		stanumbers = PointerGetDatum(arry);
+
+		bool		converted = false;
+		Datum		stavalues;
+
+		stavalues = cast_stavalues(&finfo, range_length_hist_datum, FLOAT8OID,
+								   0, &converted);
+
+		if (converted &&
+			array_check(stavalues, false, range_length_hist_name))
+		{
+			values[Anum_pg_statistic_staop1 - 1 + k] = staop;
+			values[Anum_pg_statistic_stakind1 - 1 + k] = stakind;
+			values[Anum_pg_statistic_stacoll1 - 1 + k] = stacoll;
+			values[Anum_pg_statistic_stanumbers1 - 1 + k] = stanumbers;
+			values[Anum_pg_statistic_stavalues1 - 1 + k] = stavalues;
+			k++;
+		}
+	}
+
+	/* fill in all remaining slots */
+	while (k < STATISTIC_NUM_SLOTS)
+	{
+		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;
+
+		k++;
+	}
+
+	update_pg_statistic(values, nulls);
+
+	relation_close(rel, NoLock);
+	PG_RETURN_BOOL(true);
+}
+
+/*
+ * Import statistics for a given relation attribute.
+ *
+ * This will insert/replace a row in pg_statistic for the given combinarion
+ * of relation, attribute, and inherited flag.
+ *
+ * The version parameter is for future use in the events as future versions
+ * may change them meaning of certain parameters.
+ *
+ * The variadic parameters all represent name-value pairs, with the names
+ * corresponding to attributes in pg_stats. Unkown names will generate a
+ * warning.
+ *
+ * Parameters null_frac, avg_width, and n_distinct are required because
+ * those attributes have no default value in pg_statistic.
+ *
+ * The remaining parameters all belong to a specific stakind, and all are
+ * optional. Some stakinds have multiple parameters, and in those cases
+ * both parameters must be specified if one of them is, otherwise a
+ * warning is generated but the rest of the stats may still be imported.
+ *
+ * If there is no attribute with a matching attname in the relation, the
+ * function will raise a warning and return false.
+ *
+ * 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.
+ */
+Datum
+pg_restore_attribute_stats(PG_FUNCTION_ARGS)
+{
+	/* build argument values to build the object */
+	Datum	   *args;
+	bool	   *argnulls;
+	Oid		   *types;
+	int			nargs;
+
+	LOCAL_FCINFO(set_fcinfo, 17);
+	Datum		result;
+
+	InitFunctionCallInfoData(*set_fcinfo, NULL, 17, InvalidOid, NULL, NULL);
+
+	/* line up positional arguments for pg_set_attribute_stats */
+	for (int i = 0; i <= 3; i++)
+	{
+		set_fcinfo->args[i].value = PG_GETARG_DATUM(i);
+		set_fcinfo->args[i].isnull = PG_ARGISNULL(i);
+	}
+
+	/* initialize remaining arguments which may not get set */
+	for (int i = 4; i < 17; i++)
+	{
+		set_fcinfo->args[i].value = (Datum) 0;
+		set_fcinfo->args[i].isnull = true;
+	}
+
+	nargs = extract_variadic_args(fcinfo, 4, true, &args, &types, &argnulls);
+
+	/* if the pairs aren't pairs, something is malformed */
+	if (nargs % 2 == 1)
+	{
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("stats parameters must be in name-value pairs")));
+		PG_RETURN_BOOL(false);
+	}
+
+	/* iterate through variagic arguments, putting them in set_fcinfo */
+	for (int i = 0; i < nargs; i += 2)
+	{
+		char	   *statname;
+		int			argidx = i + 1;
+
+		if (types[i] != TEXTOID)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("stat names must be of type text")));
+			break;
+		}
+
+		statname = TextDatumGetCString(args[i]);
+
+		if (strcmp(statname, null_frac_name) == 0)
+		{
+			if (types[argidx] == FLOAT4OID)
+			{
+				set_fcinfo->args[4].value = args[argidx];
+				set_fcinfo->args[4].isnull = argnulls[argidx];
+			}
+			else
+			{
+				/* required param, not recoverable */
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be of type %s",
+								null_frac_name, "real")));
+				PG_RETURN_BOOL(false);
+			}
+			continue;
+		}
+		else if (strcmp(statname, avg_width_name) == 0)
+		{
+			if (types[argidx] == INT4OID)
+			{
+				set_fcinfo->args[5].value = args[argidx];
+				set_fcinfo->args[5].isnull = argnulls[argidx];
+			}
+			else
+			{
+				/* required param, not recoverable */
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be of type %s",
+								avg_width_name, "integer")));
+				PG_RETURN_BOOL(false);
+			}
+			continue;
+		}
+		else if (strcmp(statname, n_distinct_name) == 0)
+		{
+			if (types[argidx] == FLOAT4OID)
+			{
+				set_fcinfo->args[6].value = args[argidx];
+				set_fcinfo->args[6].isnull = argnulls[argidx];
+			}
+			else
+			{
+				/* required param, not recoverable */
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be of type %s",
+								n_distinct_name, "real")));
+				PG_RETURN_BOOL(false);
+			}
+			continue;
+		}
+		else if (strcmp(statname, mc_vals_name) == 0)
+		{
+			if (types[argidx] == TEXTOID)
+			{
+				set_fcinfo->args[7].value = args[argidx];
+				set_fcinfo->args[7].isnull = argnulls[argidx];
+			}
+			else
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be type %s, ignored",
+								mc_vals_name, "text")));
+			continue;
+		}
+		else if (strcmp(statname, mc_freqs_name) == 0)
+		{
+			if (get_element_type(types[argidx]) == FLOAT4OID)
+			{
+				set_fcinfo->args[8].value = args[argidx];
+				set_fcinfo->args[8].isnull = argnulls[argidx];
+			}
+			else
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be type %s, set to NULL",
+								mc_freqs_name, "real[]")));
+			continue;
+		}
+		else if (strcmp(statname, histogram_bounds_name) == 0)
+		{
+			if (types[argidx] == TEXTOID)
+			{
+				set_fcinfo->args[9].value = args[argidx];
+				set_fcinfo->args[9].isnull = argnulls[argidx];
+			}
+			else
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be type %s, set to NULL",
+								histogram_bounds_name, "text")));
+			continue;
+		}
+		else if (strcmp(statname, correlation_name) == 0)
+		{
+			if (types[argidx] == FLOAT4OID)
+			{
+				set_fcinfo->args[10].value = args[argidx];
+				set_fcinfo->args[10].isnull = argnulls[argidx];
+			}
+			else
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be of type %s, set to NULL",
+								correlation_name, "real")));
+			continue;
+		}
+		else if (strcmp(statname, mc_elems_name) == 0)
+		{
+			if (types[argidx] == TEXTOID)
+			{
+				set_fcinfo->args[11].value = args[argidx];
+				set_fcinfo->args[11].isnull = argnulls[argidx];
+			}
+			else
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be of type %s, set to NULL",
+								mc_elems_name, "real")));
+			continue;
+		}
+		else if (strcmp(statname, mc_elem_freqs_name) == 0)
+		{
+			if (get_element_type(types[argidx]) == FLOAT4OID)
+			{
+				set_fcinfo->args[12].value = args[argidx];
+				set_fcinfo->args[12].isnull = argnulls[argidx];
+			}
+			else
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be type %s, set to NULL",
+								mc_elem_freqs_name, "real[]")));
+			continue;
+		}
+		else if (strcmp(statname, elem_count_hist_name) == 0)
+		{
+			if (get_element_type(types[argidx]) == FLOAT4OID)
+			{
+				set_fcinfo->args[13].value = args[argidx];
+				set_fcinfo->args[13].isnull = argnulls[argidx];
+			}
+			else
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be type %s, set to NULL",
+								elem_count_hist_name, "real[]")));
+			continue;
+		}
+		else if (strcmp(statname, range_length_hist_name) == 0)
+		{
+			if (types[argidx] == TEXTOID)
+			{
+				set_fcinfo->args[14].value = args[argidx];
+				set_fcinfo->args[14].isnull = argnulls[argidx];
+			}
+			else
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be type %s, set to NULL",
+								range_length_hist_name, "text")));
+			continue;
+		}
+		else if (strcmp(statname, range_empty_frac_name) == 0)
+		{
+			if (types[argidx] == FLOAT4OID)
+			{
+				set_fcinfo->args[15].value = args[argidx];
+				set_fcinfo->args[15].isnull = argnulls[argidx];
+			}
+			else
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be type %s, set to NULL",
+								range_empty_frac_name, "real")));
+			continue;
+		}
+		else if (strcmp(statname, range_bounds_hist_name) == 0)
+		{
+			if (types[argidx] == TEXTOID)
+			{
+				set_fcinfo->args[16].value = args[argidx];
+				set_fcinfo->args[16].isnull = argnulls[argidx];
+			}
+			else
+				ereport(WARNING,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("%s must be type %s, set to NULL",
+								range_bounds_hist_name, "text")));
+			continue;
+		}
+		else
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("unknown stat name '%s', ignored", statname)));
+
+		pfree(statname);
+	}
+
+	result = (*pg_set_attribute_stats) (set_fcinfo);
+
+	PG_RETURN_DATUM(result);
+}
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..9e658fa70a
--- /dev/null
+++ b/src/test/regress/expected/stats_export_import.out
@@ -0,0 +1,870 @@
+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[]
+);
+--
+-- pg_restore_relation_stats() invokes pg_set_relation_stats
+-- pg_restore_attribute_stats() invokes pg_set_attribute_stats
+--
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+        0 |        -1 |             0
+(1 row)
+
+-- error: regclass not found
+SELECT pg_restore_relation_stats('stats_export_import.nope'::regclass,
+                             150000::integer);
+ERROR:  relation "stats_export_import.nope" does not exist
+LINE 1: SELECT pg_restore_relation_stats('stats_export_import.nope':...
+                                         ^
+-- error: all three params missing
+SELECT pg_restore_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer);
+ERROR:  function pg_restore_relation_stats(regclass, integer) does not exist
+LINE 1: SELECT pg_restore_relation_stats('stats_export_import.test':...
+               ^
+HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+-- error: reltuples, relallvisible missing
+SELECT pg_restore_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 4::integer);
+WARNING:  reltuples cannot be NULL
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- error: null value
+SELECT pg_restore_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', NULL::real,
+                             'relallvisible', 4::integer);
+WARNING:  relpages must be of type integer
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+-- error: bad relpages type
+SELECT pg_restore_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+WARNING:  relpages must be of type integer
+ pg_restore_relation_stats 
+---------------------------
+ f
+(1 row)
+
+SELECT pg_restore_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 17::integer,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+ pg_restore_relation_stats 
+---------------------------
+ t
+(1 row)
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_export_import.test'::regclass;
+ relpages | reltuples | relallvisible 
+----------+-----------+---------------
+       17 |       400 |             4
+(1 row)
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    '0'::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+WARNING:  Parameter relation OID 0 is invalid
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- error: relation null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    NULL::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+WARNING:  relation cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- error: attname null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    NULL::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+WARNING:  attname cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- error: inherited null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    NULL::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+WARNING:  inherited cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- error: null_frac null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', NULL::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+WARNING:  null_frac cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- error: avg_width null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', NULL::integer,
+    'n_distinct', 0.3::real);
+WARNING:  avg_width cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- error: avg_width null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', NULL::real);
+WARNING:  n_distinct cannot be NULL
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_export_import.test'::regclass;
+ stanullfrac | stawidth | stadistinct 
+-------------+----------+-------------
+         0.1 |        2 |         0.3
+(1 row)
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_freqs', '{0.1,0.2,0.3}'::real[]
+    );
+WARNING:  most_common_vals cannot be NULL when most_common_freqs is NOT NULL
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{1,2,3}'::text
+    );
+WARNING:  most_common_freqs cannot be NULL when most_common_vals is NOT NULL
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    '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}'::double precision[]
+    );
+WARNING:  most_common_freqs must be type real[], set to NULL
+WARNING:  most_common_freqs cannot be NULL when most_common_vals is NOT NULL
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,four,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+WARNING:  invalid input syntax for type integer: "four"
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    '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_restore_attribute_stats 
+----------------------------
+ t
+(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)
+
+-- warn: histogram elements null value
+-- this generates no warnings, but perhaps it should
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,NULL,3,4}'::text
+    );
+WARNING:  histogram_bounds array cannot contain NULL values
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,2,3,4}'::text
+    );
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(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)
+
+-- ok: correlation
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'correlation', 0.5::real);
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(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)
+
+-- warn: scalars can't have mcelem
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    '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[]
+    );
+WARNING:  Relation test attname id is a scalar type, cannot have stats of type most_common_elems, ignored
+WARNING:  Relation test attname id is a scalar type, cannot have stats of type most_common_elem_freqs, ignored
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,two}'::text
+    );
+WARNING:  most_common_elem_freqs cannot be NULL when most_common_elems is NOT NULL
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
+    );
+WARNING:  most_common_elems cannot be NULL when most_common_elem_freqs is NOT NULL
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- ok: mcelem
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    '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_restore_attribute_stats 
+----------------------------
+ t
+(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)
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    '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[]
+    );
+WARNING:  Relation test attname id is a scalar type, cannot have stats of type elem_count_histogram, ignored
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    '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[]
+    );
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    '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_restore_attribute_stats 
+----------------------------
+ t
+(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)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    '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
+    );
+WARNING:  Relation test attname id is not a range type, cannot have stats of type range_length_histogram
+WARNING:  Relation test attname id is not a range type, cannot have stats of type range_empty_frac
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+WARNING:  range_empty_frac cannot be NULL when range_length_histogram is NOT NULL
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real
+    );
+WARNING:  range_length_histogram cannot be NULL when range_empty_frac is NOT NULL
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    '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_restore_attribute_stats 
+----------------------------
+ t
+(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)
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    '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
+    );
+WARNING:  Relation test attname id is not a range type, cannot have stats of type range_bounds_histogram
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    '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_restore_attribute_stats 
+----------------------------
+ t
+(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)
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    '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
+    );
+WARNING:  imported statistics must have a maximum of 5 slots but 6 given
+ pg_restore_attribute_stats 
+----------------------------
+ f
+(1 row)
+
+--
+-- Test the ability to exactly copy data from one table to an identical table,
+-- correctly reconstructing the stakind order as well as the staopN and
+-- 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_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+ pg_restore_attribute_stats 
+----------------------------
+ t
+(1 row)
+
+SELECT c.relname, COUNT(*) AS num_stats
+FROM pg_class AS c
+JOIN pg_statistic s ON s.starelid = c.oid
+WHERE c.relnamespace = 'stats_export_import'::regnamespace
+AND c.relname IN ('test', 'test_clone', 'is_odd', 'is_odd_clone')
+GROUP BY c.relname
+ORDER BY c.relname;
+   relname    | num_stats 
+--------------+-----------
+ is_odd       |         1
+ is_odd_clone |         1
+ test         |         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 2429ec2bba..ea99302741 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -28,7 +28,7 @@ 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 database
+test: geometry horology tstypes regex type_sanity opr_sanity misc_sanity comments expressions unicode xid mvcc database 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..f781eec5dd
--- /dev/null
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -0,0 +1,677 @@
+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[]
+);
+
+--
+-- pg_restore_relation_stats() invokes pg_set_relation_stats
+-- pg_restore_attribute_stats() invokes pg_set_attribute_stats
+
+--
+SELECT relpages, reltuples, relallvisible FROM pg_class WHERE oid = 'stats_export_import.test'::regclass;
+
+-- error: regclass not found
+SELECT pg_restore_relation_stats('stats_export_import.nope'::regclass,
+                             150000::integer);
+
+-- error: all three params missing
+SELECT pg_restore_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer);
+
+-- error: reltuples, relallvisible missing
+SELECT pg_restore_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 4::integer);
+
+-- error: null value
+SELECT pg_restore_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', NULL::real,
+                             'relallvisible', 4::integer);
+
+-- error: bad relpages type
+SELECT pg_restore_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 'nope'::text,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+
+SELECT pg_restore_relation_stats('stats_export_import.test'::regclass,
+                             150000::integer,
+                             'relpages', 17::integer,
+                             'reltuples', 400.0::real,
+                             'relallvisible', 4::integer);
+
+SELECT relpages, reltuples, relallvisible
+FROM pg_class
+WHERE oid = 'stats_export_import.test'::regclass;
+
+-- error: object doesn't exist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    '0'::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: relation null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    NULL::oid,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: attname null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    NULL::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: inherited null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    NULL::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: null_frac null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', NULL::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', NULL::integer,
+    'n_distinct', 0.3::real);
+
+-- error: avg_width null
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', NULL::real);
+
+-- ok: no stakinds
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.1::real,
+    'avg_width', 2::integer,
+    'n_distinct', 0.3::real);
+
+SELECT stanullfrac, stawidth, stadistinct
+FROM pg_statistic
+WHERE starelid = 'stats_export_import.test'::regclass;
+
+-- warn: mcv / mcf null mismatch
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_freqs', '{0.1,0.2,0.3}'::real[]
+    );
+
+-- warn: mcv / mcf null mismatch part 2
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{1,2,3}'::text
+    );
+
+-- warn: mcv / mcf type mismatch
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    '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}'::double precision[]
+    );
+
+-- warning: mcv cast failure
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_vals', '{2,four,3}'::text,
+    'most_common_freqs', '{0.3,0.25,0.05}'::real[]
+    );
+
+-- ok: mcv+mcf
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    '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';
+
+-- warn: histogram elements null value
+-- this generates no warnings, but perhaps it should
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'histogram_bounds', '{1,NULL,3,4}'::text
+    );
+
+-- ok: histogram_bounds
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    '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';
+
+-- ok: correlation
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    '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';
+
+-- warn: scalars can't have mcelem
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{1,3}'::text,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3,0.0}'::real[]
+    );
+
+-- warn: mcelem / mcelem mismatch
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elems', '{one,two}'::text
+    );
+
+-- warn: mcelem / mcelem null mismatch part 2
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'most_common_elem_freqs', '{0.3,0.2,0.2,0.3}'::real[]
+    );
+
+-- ok: mcelem
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    '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';
+
+-- warn: scalars can't have elem_count_histogram
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- warn: elem_count_histogram null element
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'elem_count_histogram', '{1,1,NULL,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}'::real[]
+    );
+-- ok: elem_count_histogram
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'tags'::name,
+    false::boolean,
+    150000::integer,
+    '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';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_length_histogram', '{399,499,Infinity}'::text
+    );
+-- warn: range_empty_frac range_length_hist null mismatch part 2
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_empty_frac', 0.5::real
+    );
+-- ok: range_empty_frac + range_length_hist
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    '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';
+
+-- warn: scalars can't have range stats
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'id'::name,
+    false::boolean,
+    150000::integer,
+    'null_frac', 0.5::real,
+    'avg_width', 2::integer,
+    'n_distinct', -0.1::real,
+    'range_bounds_histogram', '{"[-1,1)","[0,4)","[1,4)","[1,100)"}'::text
+    );
+-- ok: range_bounds_histogram
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    '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';
+
+-- warn: exceed STATISTIC_NUM_SLOTS
+SELECT pg_catalog.pg_restore_attribute_stats(
+    'stats_export_import.test'::regclass,
+    'arange'::name,
+    false::boolean,
+    150000::integer,
+    '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_restore_attribute_stats( '
+            || '%L::regclass::oid, '
+            || '%L::name, '
+            || '%L::boolean, '
+            || '%L::integer, '
+            || '%L, %L::real, '
+            || '%L, %L::integer, '
+            || '%L, %L::real %s)',
+        'stats_export_import.' || s.tablename || '_clone',
+        s.attname,
+        s.inherited,
+        150000,
+        'null_frac', s.null_frac,
+        'avg_width', s.avg_width,
+        'n_distinct', s.n_distinct,
+        CASE
+            WHEN s.most_common_vals IS NULL THEN ''
+            ELSE format(', %L, %L::text, %L, %L::real[]',
+                        'most_common_vals', s.most_common_vals,
+                        'most_common_freqs', s.most_common_freqs)
+        END ||
+        CASE
+            WHEN s.histogram_bounds IS NULL THEN ''
+            ELSE format(', %L, %L::text',
+                        'histogram_bounds', s.histogram_bounds)
+        END ||
+        CASE
+            WHEN s.correlation IS NULL THEN ''
+            ELSE format(', %L, %L::real',
+                        'correlation', s.correlation)
+        END ||
+        CASE
+            WHEN s.most_common_elems IS NULL THEN ''
+            ELSE format(', %L, %L::text, %L, %L::real[]',
+                        'most_common_elems', s.most_common_elems,
+                        'most_common_elem_freqs', s.most_common_elem_freqs)
+        END ||
+        CASE
+            WHEN s.elem_count_histogram IS NULL THEN ''
+            ELSE format(', %L, %L::real[]',
+                        'elem_count_histogram', s.elem_count_histogram)
+        END ||
+        CASE
+            WHEN s.range_bounds_histogram IS NULL THEN ''
+            ELSE format(', %L, %L::text',
+                        'range_bounds_histogram', s.range_bounds_histogram)
+        END ||
+        CASE
+            WHEN s.range_empty_frac IS NULL THEN ''
+            ELSE format(', %L, %L::real, %L, %L::text',
+                        'range_empty_frac', s.range_empty_frac,
+                        'range_length_histogram', s.range_length_histogram)
+        END)
+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 3f93c61aa3..896f87bf74 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -29761,6 +29761,405 @@ 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>version</parameter> <type>integer</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 value of <structfield>version</structfield> represents the
+        integer <term><varname>SERVER_VERSION_NUM</varname></term> of the
+        database that was the source of these values. A value of
+        <literal>NULL</literal> means to use the
+        <term><varname>SERVER_VERSION_NUM</varname></term> of the
+        current database. It should be noted that presently this value does
+        not alter the behavior of the function, but it could in future
+        versions.
+       </para>
+       <para>
+        The value of <structfield>relpages</structfield> must not be less than
+        0.
+       </para>
+       <para>
+        The value of <structfield>reltuples</structfield> must not be less than
+        -1.0.
+       </para>
+       <para>
+        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>version</parameter> <type>integer</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>.
+       </para>
+       <para>
+        The value of <structfield>version</structfield> represents the
+        integer <term><varname>SERVER_VERSION_NUM</varname></term> of the
+        database that was the source of these values. A value of
+        <literal>NULL</literal> means to use the
+        <term><varname>SERVER_VERSION_NUM</varname></term> of the
+        current database. It should be noted that presently this value does
+        not alter the behavior of the function, but it could in future
+        versions.
+       </para>
+       <para>
+        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>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_restore_relation_stats</primary>
+        </indexterm>
+        <function>pg_restore_relation_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>,
+         <parameter>version</parameter> <type>integer</type>,
+         <literal>VARIADIC</literal> <parameter>stats</parameter> <type>"any"</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Updates the <structname>pg_class</structname> row for the specified
+        <parameter>relation</parameter>, values from the variadic list of
+        name-value pairs. Each parameter name corresponds to the same-named
+        attribute in <structname>pg_class</structname>:
+       </para>
+       <variablelist>
+        <varlistentry>
+         <term><literal>relpages</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>integer</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>reltuples</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>relalltuples</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>integer</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+       </variablelist>
+       <para>
+        Any other parameter names given must also have a value pair, but will emit
+        a warning and thereafter be ignored.
+       </para>
+       <para>
+        The <parameter>version</parameter> is meant to reflect the server version
+        number of the system where this data was generated, as that may in the
+        future change how the data is imported.
+       </para>
+       <para>
+        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 given are checked for type validity and must all be
+        NOT NULL.
+       </para>
+       <para>
+        The parameters will return true if stats were updated and false if not.
+       </para>
+       </entry>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_restore_attribute_stats</primary>
+        </indexterm>
+        <function>pg_restore_attribute_stats</function> (
+         <parameter>relation</parameter> <type>regclass</type>
+         , <parameter>attname</parameter> <type>name</type>
+         , <parameter>inherited</parameter> <type>boolean</type>
+         , <parameter>version</parameter> <type>integer</type>
+         , <literal>VARIADIC</literal> <parameter>stats</parameter> <type>"any"</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Replaces the <structname>pg_statistic</structname> row for the
+        <structname>pg_attribute</structname> row specified by
+        <parameter>relation</parameter>, <parameter>attname</parameter>
+        and <parameter>inherited</parameter>. The <parameter>version</parameter>
+        should be set to the server version number of the server where
+        the variadic <parameter>stats</parameter> originated, as it may in the
+        future affect how the stats are imported. Values from the variadic
+        list form name-value pairs. Each parameter name corresponds to the
+        same-named attribute in
+        <link linkend="view-pg-stats"><structname>pg_stats</structname></link>:
+       </para>
+       <variablelist>
+        <varlistentry>
+         <term><literal>null_frac</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>avg_width</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>integer</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>n_distinct</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+           This parameter is currently required.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_vals</literal></term>
+         <listitem>
+          <para>
+          <literal>most_common_vals</literal>
+           Must be followed by a parameter of type <type>text</type>. If this
+           parameter is specified then <literal>most_common_freqs</literal>
+           must also be specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_freqs</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of <type>real[]</type>, and can only
+           be specified if <literal>most_common_vals</literal> is also specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>histogram_bounds</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>correlation</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_elems</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>. If this
+           parameter is specified then <literal>most_common_elem_freqs</literal>
+           must also be specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>most_common_elem_freqs</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real[]</type>, and can
+           only be specified if <literal>most_common_elems</literal> is also
+           specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>elem_count_histogram</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real[]</type>.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>range_length_histogram</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>. If this
+           parameter is specified then <literal>range_empty_frac</literal>
+           must also be specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>range_empty_frac</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>real</type>, and can
+           only be specified if <literal>range_length_histogram</literal>
+           is also specified.
+          </para>
+         </listitem>
+        </varlistentry>
+        <varlistentry>
+         <term><literal>range_bounds_histogram</literal></term>
+         <listitem>
+          <para>
+           Must be followed by a parameter of type <type>text</type>
+          </para>
+         </listitem>
+        </varlistentry>
+       </variablelist>
+       <para>
+        Any other parameter names given must also have a value pair, but will emit
+        a warning and thereafter be ignored.
+       </para>
+       <para>
+        The parameters will return true if stats were updated and false if not.
+       </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>.
+       </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.45.2