Subject: Re: Statistics Import and Export

From: Corey Huinker <[email-masked]>
Date: Fri, 2 Feb 2024 03:37:10 -0500

(hit send before attaching patches, reposting message as well)

Attached is v4 of the statistics export/import patch.

This version has been refactored to match the design feedback received
previously.

The system views are gone. These were mostly there to serve as a baseline
for what an export query would look like. That role is temporarily
reassigned to pg_export_stats.c, but hopefully they will be integrated into
pg_dump in the next version. The regression test also contains the version
of each query suitable for the current server version.

The export format is far closer to the raw format of pg_statistic and
pg_statistic_ext_data, respectively. This format involves exporting oid
values for types, collations, operators, and attributes - values which are
specific to the server they were created on. To make sense of those values,
a subset of the columns of pg_type, pg_attribute, pg_collation, and
pg_operator are exported as well, which allows pg_import_rel_stats() and
pg_import_ext_stats() to reconstitute the data structure as it existed on
the old server, and adapt it to the modern structure and local schema
objects.

pg_import_rel_stats matches up local columns with the exported stats by
column name, not attnum. This allows for stats to be imported when columns
have been dropped, added, or reordered.

pg_import_ext_stats can also handle column reordering, though it currently
would get confused by changes in expressions that maintain the same result
data type. I'm not yet brave enough to handle importing nodetrees, nor do I
think it's wise to try. I think we'd be better off validating that the
destination extended stats object is identical in structure, and to fail
the import of that one object if it isn't perfect.

Export formats go back to v10.

On Mon, Jan 22, 2024 at 1:09 AM Peter Smith <[email-masked]> wrote:

> 2024-01 Commitfest.
>
> Hi, This patch has a CF status of "Needs Review" [1], but it seems
> there were CFbot test failures last time it was run [2]. Please have a
> look and post an updated version if necessary.
>
> ======
> [1] https://commitfest.postgresql.org/46/4538/
> [2]
> https://cirrus-ci.com/github/postgresql-cfbot/postgresql/commitfest/46/4538
>
> Kind Regards,
> Peter Smith.
>
From 6bdc7076d20ff4cd71b851ea34f8ffa69fe03f67 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Fri, 2 Feb 2024 00:16:04 -0500
Subject: [PATCH v4 1/3] Create pg_import_rel_stats.

The function pg_import_rel_stats imports pg_class rowcount,
pagecount, and pg_statistic data for a given relation.

The most likely application of this function is to quickly apply stats
to a newly upgraded database faster than could be accomplished by
vacuumdb --analyze-in-stages.

The function takes a jsonb parameter which contains the generated
statistics for one relaton, the format of which varies by the version
of the server that exported it. The function takes that version
int account when processing the input json into pg_statistic rows.

The statistics applied are not locked in any way, and will be
overwritten by the next analyze, either explicit or via autovacuum.

While the statistics are applied transactionally, the changes to
pg_class (reltuples and relpages) are not. This decision was made
to avoid bloat of pg_class and is in line with the behavior of VACUUM.

Currently the function supports two boolean flags for checking the
validity of the imported data. The flag validate initiates a battery
of validation tests to ensure that all sub-objects (types, operators,
collatons, attributes, statistics) have no duplicate values. The flag
require_match_oids verifies the oids resolved in the new statistics rows
match the oids specified in the json. Setting this flag makes sense
during a binary upgrade, but not a restore.

This function also allows for tweaking of table statistics in-place,
allowing the user to inflate rowcounts, skew histograms, etc, to see
what those changes will evoke from the query planner.
---
 src/include/catalog/pg_proc.dat               |    6 +-
 src/include/statistics/statistics.h           |   20 +
 src/backend/statistics/Makefile               |    3 +-
 src/backend/statistics/meson.build            |    1 +
 src/backend/statistics/statistics.c           | 1390 +++++++++++++++++
 .../regress/expected/stats_export_import.out  |  530 +++++++
 src/test/regress/parallel_schedule            |    2 +-
 src/test/regress/sql/stats_export_import.sql  |  499 ++++++
 doc/src/sgml/func.sgml                        |   56 +
 9 files changed, 2504 insertions(+), 3 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 29af4ce65d..ec8ce7c3c0 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -8825,7 +8825,11 @@
 { oid => '3813', descr => 'generate XML text node',
   proname => 'xmltext', proisstrict => 't', prorettype => 'xml',
   proargtypes => 'text', prosrc => 'xmltext' },
-
+{ oid => '3814',
+  descr => 'statistics: import to relation',
+  proname => 'pg_import_rel_stats', provolatile => 'v', proisstrict => 'f',
+  proparallel => 'u', prorettype => 'bool', proargtypes => 'oid jsonb bool bool',
+  prosrc => 'pg_import_rel_stats' },
 { oid => '2923', descr => 'map table contents to XML',
   proname => 'table_to_xml', procost => '100', provolatile => 's',
   proparallel => 'r', prorettype => 'xml',
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 7f2bf18716..11a213e21a 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -127,4 +127,24 @@ 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_import_rel_stats(PG_FUNCTION_ARGS);
+
+extern VacAttrStats *examine_rel_attribute(Relation onerel, int attnum,
+										   Node *index_expr);
+
+extern HeapTuple *import_pg_statistics(Relation rel, Relation sd,
+									   int server_version_num,
+									   const Datum *datums, const bool *nulls,
+									   bool require_match_oids, int *ntuples);
+
+extern void validate_no_duplicates(Datum document, bool document_null,
+								   const char *sql, const char *docname,
+								   const char *colname);
+
+extern void validate_exported_types(Datum types, bool types_null);
+extern void validate_exported_collations(Datum collations, bool collations_null);
+extern void validate_exported_operators(Datum operators, bool operators_null);
+extern void validate_exported_attributes(Datum attributes, bool attributes_null);
+extern void validate_exported_statistics(Datum statistics, bool statistics_null);
+
 #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..6cba780691
--- /dev/null
+++ b/src/backend/statistics/statistics.c
@@ -0,0 +1,1390 @@
+/*-------------------------------------------------------------------------
+ *
+ * statistics.c
+ *
+ * IDENTIFICATION
+ *       src/backend/statistics/statistics.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/heapam.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_type.h"
+#include "executor/spi.h"
+#include "fmgr.h"
+#include "nodes/nodeFuncs.h"
+#include "parser/parse_oper.h"
+#include "statistics/statistics.h"
+#include "utils/builtins.h"
+#include "utils/rel.h"
+#include "utils/fmgroids.h"
+#include "utils/lsyscache.h"
+#include "utils/syscache.h"
+
+/*
+ * Struct to capture only the infomration we need from
+ * examine_attribute.
+ */
+typedef struct {
+	Oid		typid;
+	int32	typmod;
+	Oid 	eqopr;
+	Oid 	ltopr;
+	Oid		basetypid;
+	Oid 	baseeqopr;
+	Oid 	baseltopr;
+} AttrInfo;
+
+
+/*
+ * Generate AttrInfo entries for each attribute in the relation.
+ * This data is a small subset of what VacAttrStats collects,
+ * and we leverage VacAttrStats to stay compatible with what
+ * do_analyze() does.
+ */
+static AttrInfo *
+get_attrinfo(Relation rel)
+{
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	int			natts = tupdesc->natts;
+	bool		has_index_exprs = false;
+	ListCell   *indexpr_item = NULL;
+	AttrInfo   *res = palloc0(natts * sizeof(AttrInfo));
+	int			i;
+
+	/*
+	 * If this relation is an index and that index has expressions in
+	 * it, then we will need to keep the list of remaining expressions
+	 * aligned with the attributes as we iterate over them, whether or
+	 * not those attributes have statistics to import.
+	*/
+	if ((rel->rd_rel->relkind == RELKIND_INDEX
+				|| (rel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+			&& (rel->rd_indexprs != NIL))
+	{
+		has_index_exprs = true;
+		indexpr_item = list_head(rel->rd_indexprs);
+	}
+
+	for (i = 0; i < natts; i++)
+	{
+		Node *index_expr = NULL;
+		VacAttrStats *stats;
+
+		/*
+		 * If this this attribute is an expression, pop an expression off
+		 * of the list.
+		 */
+		if (has_index_exprs && (rel->rd_index->indkey.values[i] == 0))
+		{
+			if (indexpr_item == NULL)   /* shouldn't happen */
+				elog(ERROR, "too few entries in indexprs list");
+
+			index_expr = (Node *) lfirst(indexpr_item);
+			indexpr_item = lnext(rel->rd_indexprs, indexpr_item);
+		}
+
+		stats = examine_rel_attribute(rel, i+1, index_expr);
+
+		res[i].typid = stats->attrtypid;
+		res[i].typmod = stats->attrtypmod;
+		get_sort_group_operators(res[i].typid,
+								 false, false, false,
+								 &res[i].ltopr, &res[i].eqopr, NULL,
+								 NULL);
+
+		get_sort_group_operators(res[i].typid,
+								 false, false, false,
+								 &res[i].ltopr, &res[i].eqopr, NULL,
+								 NULL);
+
+		res[i].basetypid = get_base_element_type(stats->attrtypid);
+		if (res[i].basetypid == InvalidOid)
+		{
+			/* type is its own base type */
+			res[i].basetypid = res[i].typid;
+			res[i].baseltopr = res[i].ltopr;
+			res[i].baseeqopr = res[i].eqopr;
+		}
+		else
+			get_sort_group_operators(res[i].basetypid,
+									 false, false, false,
+									 &res[i].baseltopr, &res[i].baseeqopr,
+									 NULL, NULL);
+
+	}
+	return res;
+}
+
+/*
+ * examine_rel_attribute -- pre-analysis of a single column
+ *
+ * Determine whether the column is analyzable; if so, create and initialize
+ * a VacAttrStats struct for it.  If not, return NULL.
+ *
+ * If index_expr isn't NULL, then we're trying to import an expression index,
+ * and index_expr is the expression tree representing the column's data.
+ */
+VacAttrStats *
+examine_rel_attribute(Relation onerel, int attnum, Node *index_expr)
+{
+	Form_pg_attribute attr = TupleDescAttr(onerel->rd_att, attnum - 1);
+	HeapTuple		typtuple;
+	VacAttrStats   *stats;
+	int				i;
+	bool			ok;
+
+	/* Never analyze dropped columns */
+	if (attr->attisdropped)
+		return NULL;
+
+	/*
+	 * Create the VacAttrStats struct.
+	 */
+	stats = (VacAttrStats *) palloc0(sizeof(VacAttrStats));
+	stats->attstattarget = 1; /* Any nonzero value */
+
+	/*
+	 * When analyzing an expression index, believe the expression tree's type
+	 * not the column datatype --- the latter might be the opckeytype storage
+	 * type of the opclass, which is not interesting for our purposes.  (Note:
+	 * if we did anything with non-expression index columns, we'd need to
+	 * figure out where to get the correct type info from, but for now that's
+	 * not a problem.)	It's not clear whether anyone will care about the
+	 * typmod, but we store that too just in case.
+	 */
+	if (index_expr)
+	{
+		stats->attrtypid = exprType(index_expr);
+		stats->attrtypmod = exprTypmod(index_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(onerel->rd_indcollation[attnum - 1]))
+			stats->attrcollid = onerel->rd_indcollation[attnum - 1];
+		else
+			stats->attrcollid = exprCollation(index_expr);
+	}
+	else
+	{
+		stats->attrtypid = attr->atttypid;
+		stats->attrtypmod = attr->atttypmod;
+		stats->attrcollid = attr->attcollation;
+	}
+
+	typtuple = SearchSysCacheCopy1(TYPEOID,
+								   ObjectIdGetDatum(stats->attrtypid));
+	if (!HeapTupleIsValid(typtuple))
+		elog(ERROR, "cache lookup failed for type %u", stats->attrtypid);
+	stats->attrtype = (Form_pg_type) GETSTRUCT(typtuple);
+	stats->anl_context = NULL;
+	stats->tupattnum = attnum;
+
+	/*
+	 * The fields describing the stats->stavalues[n] element types default to
+	 * the type of the data being analyzed, but the type-specific typanalyze
+	 * function can change them if it wants to store something else.
+	 */
+	for (i = 0; i < STATISTIC_NUM_SLOTS; i++)
+	{
+		stats->statypid[i] = stats->attrtypid;
+		stats->statyplen[i] = stats->attrtype->typlen;
+		stats->statypbyval[i] = stats->attrtype->typbyval;
+		stats->statypalign[i] = stats->attrtype->typalign;
+	}
+
+	/*
+	 * Call the type-specific typanalyze function.  If none is specified, use
+	 * std_typanalyze().
+	 */
+	if (OidIsValid(stats->attrtype->typanalyze))
+		ok = DatumGetBool(OidFunctionCall1(stats->attrtype->typanalyze,
+										   PointerGetDatum(stats)));
+	else
+		ok = std_typanalyze(stats);
+
+	if (!ok || stats->compute_stats == NULL || stats->minrows <= 0)
+	{
+		heap_freetuple(typtuple);
+		pfree(stats);
+		return NULL;
+	}
+
+	return stats;
+}
+
+/*
+ * Delete all pg_statistic entries for a relation + inheritance type
+ */
+static void
+remove_pg_statistics(Relation rel, Relation sd, bool inh)
+{
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	int			natts = tupdesc->natts;
+	int			attnum;
+
+	for (attnum = 1; attnum <= natts; attnum++)
+	{
+		HeapTuple tup = SearchSysCache3(STATRELATTINH,
+							ObjectIdGetDatum(RelationGetRelid(rel)),
+							Int16GetDatum(attnum),
+							BoolGetDatum(inh));
+
+		if (HeapTupleIsValid(tup))
+		{
+			CatalogTupleDelete(sd, &tup->t_self);
+
+			ReleaseSysCache(tup);
+		}
+	}
+}
+
+#define NULLARG(x) ((x) ? 'n' : ' ')
+
+/*
+ * A common pattern of duplicate detection.
+ *
+ * This function assumes a valid SPI connection.
+ */
+void
+validate_no_duplicates(Datum document, bool document_null,
+					   const char *sql, const char *docname,
+					   const char *colname)
+{
+	Oid		argtypes[1] = { JSONBOID };
+	Datum	args[1] = { document };
+	char	argnulls[1] = { NULLARG(document_null) };
+
+	SPITupleTable  *tuptable;
+	int				ret;
+
+	ret = SPI_execute_with_args(sql, 1, argtypes, args, argnulls, true, 0);
+
+	if (ret != SPI_OK_SELECT)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("statistic export JSON is not in proper format")));
+
+	tuptable = SPI_tuptable;
+	if (tuptable->numvals > 0)
+	{
+		char *s = SPI_getvalue(tuptable->vals[0], tuptable->tupdesc, 1);
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("statistic export JSON document \"%s\" has duplicate rows with %s = %s",
+						docname, colname, (s) ? s : "NULL")));
+	}
+}
+
+/*
+ * Ensure that the "types" document is valid.
+ *
+ * This function assumes a valid SPI connection.
+ */
+void
+validate_exported_types(Datum types, bool types_null)
+{
+	const char *sql =
+		"SELECT et.oid "
+		"FROM jsonb_to_recordset($1) "
+		"     AS et(oid oid, typname text, nspname text) "
+		"GROUP BY et.oid "
+		"HAVING COUNT(*) > 1 ";
+
+	validate_no_duplicates(types, types_null, sql, "types", "oid");
+}
+
+/*
+ * Ensure that the "collations" document is valid.
+ *
+ * This function assumes a valid SPI connection.
+ */
+void
+validate_exported_collations(Datum collations, bool collations_null)
+{
+	const char* sql =
+		"SELECT ec.oid "
+		"FROM jsonb_to_recordset($1) "
+		"     AS ec(oid oid, collname text, nspname text) "
+		"GROUP BY ec.oid "
+		"HAVING COUNT(*) > 1 ";
+
+	validate_no_duplicates(collations, collations_null, sql, "collations", "oid");
+}
+
+/*
+ * Ensure that the "operators" document is valid.
+ *
+ * This function assumes a valid SPI connection.
+ */
+void
+validate_exported_operators(Datum operators, bool operators_null)
+{
+	const char* sql =
+		"SELECT eo.oid "
+		"FROM jsonb_to_recordset($1) "
+		"     AS eo(oid oid, oprname text, nspname text) "
+		"GROUP BY eo.oid "
+		"HAVING COUNT(*) > 1 ";
+
+	validate_no_duplicates(operators, operators_null, sql, "operators", "oid");
+}
+
+/*
+ * Ensure that the "attributes" document is valid.
+ *
+ * This function assumes a valid SPI connection.
+ */
+void
+validate_exported_attributes(Datum attributes, bool attributes_null)
+{
+	const char* sql =
+		"SELECT ea.attnum "
+		"FROM jsonb_to_recordset($1) "
+		"     AS ea(attnum int2, attname text, atttypid oid, "
+		"           attcollation oid) "
+		"GROUP BY ea.attnum "
+		"HAVING COUNT(*) > 1 ";
+
+	validate_no_duplicates(attributes, attributes_null, sql, "attributes", "attnum");
+}
+
+/*
+ * Ensure that the "statistics" document is valid.
+ *
+ * This function assumes a valid SPI connection.
+ */
+void
+validate_exported_statistics(Datum statistics, bool statistics_null)
+{
+	Oid		argtypes[1] = { JSONBOID };
+	Datum	args[1] = { statistics };
+	char	argnulls[1] = { NULLARG(statistics_null) };
+
+	const char *sql =
+		"SELECT s.staattnum, s.stainherit "
+		"FROM jsonb_to_recordset($1) "
+		"    AS s(staattnum integer, "
+		"         stainherit boolean, "
+		"         stanullfrac float4, "
+		"         stawidth integer, "
+		"         stadistinct float4, "
+		"         stakind1 int2, "
+		"         stakind2 int2, "
+		"         stakind3 int2, "
+		"         stakind4 int2, "
+		"         stakind5 int2, "
+		"         staop1 oid, "
+		"         staop2 oid, "
+		"         staop3 oid, "
+		"         staop4 oid, "
+		"         staop5 oid, "
+		"         stacoll1 oid, "
+		"         stacoll2 oid, "
+		"         stacoll3 oid, "
+		"         stacoll4 oid, "
+		"         stacoll5 oid, "
+		"         stanumbers1 float4[], "
+		"         stanumbers2 float4[], "
+		"         stanumbers3 float4[], "
+		"         stanumbers4 float4[], "
+		"         stanumbers5 float4[], "
+		"         stavalues1 text, "
+		"         stavalues2 text, "
+		"         stavalues3 text, "
+		"         stavalues4 text, "
+		"         stavalues5 text) "
+		"GROUP BY s.staattnum, s.stainherit "
+		"HAVING COUNT(*) > 1 ";
+
+	SPITupleTable  *tuptable;
+	int				ret;
+
+	ret = SPI_execute_with_args(sql, 1, argtypes, args, argnulls, true, 0);
+
+	if (ret != SPI_OK_SELECT)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("statistic export JSON is not in proper format")));
+
+	tuptable = SPI_tuptable;
+
+	if (tuptable->numvals > 0)
+	{
+		char *s1 = SPI_getvalue(tuptable->vals[0], tuptable->tupdesc, 1);
+		char *s2 = SPI_getvalue(tuptable->vals[0], tuptable->tupdesc, 2);
+
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("statistic export JSON document \"%s\" has duplicate rows with %s = %s, %s = %s",
+						"statistics", "staattnum", (s1) ? s1 : "NULL",
+						"stainherit", (s2) ? s2 : "NULL")));
+	}
+}
+
+
+/*
+ * Transactionally import statistics for a given relation
+ * into pg_statistic.
+ *
+ * The jsonb datums are in the same order:
+ *     types, collations, operators, attributes, statistics
+ *
+ * The statistics import query does not vary by server version.
+ * However, the stacollN columns will always be NULL for versions prior
+ * to v12.
+ *
+ * The query as currently written is clearly overboard, and for now serves
+ * to show what is possible in terms of comparing the exported statistics
+ * to the existing local schema. Once we have determined what types of
+ * checks are worthwhile, we can trim out unnecessary joins and columns.
+ *
+ * Analytic columns columns like dup_count serve to check the consistency
+ * and correctness of the exported data.
+ *
+ * The return value is an array of HeapTuples.
+ * The parameter ntuples is set to the number of HeapTuples returned.
+ */
+
+HeapTuple *
+import_pg_statistics(Relation rel, Relation sd, int server_version_num,
+					 const Datum *datums, const bool *nulls,
+					 bool require_match_oids, int *ntuples)
+{
+
+#define PGS_NARGS 6
+
+	Oid			argtypes[PGS_NARGS] = {
+					JSONBOID, JSONBOID, JSONBOID, JSONBOID, JSONBOID, OIDOID };
+	Datum		args[PGS_NARGS] = {
+					datums[0], datums[1], datums[2], datums[3], datums[4],
+					ObjectIdGetDatum(RelationGetRelid(rel)) };
+	char		argnulls[PGS_NARGS] = {
+					NULLARG(nulls[0]), NULLARG(nulls[1]), NULLARG(nulls[2]),
+					NULLARG(nulls[3]), NULLARG(nulls[4]) };
+
+	/*
+	 * This query is currently in kitchen-sink mode, and it can be trimmed down
+	 * to eliminate any columns not needed for output or validation once
+	 * all requirements are settled.
+	 */
+	const char *sql =
+		"WITH exported_types AS ( "
+		"    SELECT et.* "
+		"    FROM jsonb_to_recordset($1) "
+		"        AS et(oid oid, typname text, nspname text) "
+		"), "
+		"exported_collations AS ( "
+		"    SELECT ec.* "
+		"    FROM jsonb_to_recordset($2) "
+		"        AS ec(oid oid, collname text, nspname text) "
+		"), "
+		"exported_operators AS ( "
+		"    SELECT eo.* "
+		"    FROM jsonb_to_recordset($3) "
+		"        AS eo(oid oid, oprname text, nspname text) "
+		"), "
+		"exported_attributes AS ( "
+		"    SELECT ea.* "
+		"    FROM jsonb_to_recordset($4) "
+		"        AS ea(attnum int2, attname text, atttypid oid, "
+		"              attcollation oid) "
+		"), "
+		"exported_statistics AS ( "
+		"    SELECT s.* "
+		"    FROM jsonb_to_recordset($5) "
+		"        AS s(staattnum integer, "
+		"             stainherit boolean, "
+		"             stanullfrac float4, "
+		"             stawidth integer, "
+		"             stadistinct float4, "
+		"             stakind1 int2, "
+		"             stakind2 int2, "
+		"             stakind3 int2, "
+		"             stakind4 int2, "
+		"             stakind5 int2, "
+		"             staop1 oid, "
+		"             staop2 oid, "
+		"             staop3 oid, "
+		"             staop4 oid, "
+		"             staop5 oid, "
+		"             stacoll1 oid, "
+		"             stacoll2 oid, "
+		"             stacoll3 oid, "
+		"             stacoll4 oid, "
+		"             stacoll5 oid, "
+		"             stanumbers1 float4[], "
+		"             stanumbers2 float4[], "
+		"             stanumbers3 float4[], "
+		"             stanumbers4 float4[], "
+		"             stanumbers5 float4[], "
+		"             stavalues1 text, "
+		"             stavalues2 text, "
+		"             stavalues3 text, "
+		"             stavalues4 text, "
+		"             stavalues5 text) "
+		") "
+		"SELECT pga.attnum, pga.attname, pga.atttypid, pga.atttypmod, "
+		"       pga.attcollation, pgat.typname, pgac.collname, "
+		"       ea.attnum AS exp_attnum, ea.atttypid AS exp_atttypid, "
+		"       ea.attcollation AS exp_attcollation, "
+		"       et.typname AS exp_typname, et.nspname AS exp_typschema, "
+		"       ec.collname AS exp_collname, ec.nspname AS exp_collschema, "
+		"       es.stainherit, es.stanullfrac, es.stawidth, es.stadistinct, "
+		"       es.stakind1, es.stakind2, es.stakind3, es.stakind4, "
+		"       es.stakind5, "
+		"       es.staop1 AS exp_staop1, es.staop2 AS exp_staop2, "
+		"       es.staop3 AS exp_staop3, es.staop4 AS exp_staop4, "
+		"       es.staop5 AS exp_staop5, "
+		"       es.stacoll1 AS exp_staop1, es.stacoll2 AS exp_staop2, "
+		"       es.stacoll3 AS exp_staop3, es.stacoll4 AS exp_staop4, "
+		"       es.stacoll5 AS exp_staop5, "
+		"       es.stanumbers1, es.stanumbers2, es.stanumbers3, "
+		"       es.stanumbers4, es.stanumbers5, "
+		"       es.stavalues1, es.stavalues2, es.stavalues3, es.stavalues4, "
+		"       es.stavalues5, "
+		"       eo1.nspname AS exp_oprschema1, "
+		"       eo2.nspname AS exp_oprschema2, "
+		"       eo3.nspname AS exp_oprschema3, "
+		"       eo4.nspname AS exp_oprschema4, "
+		"       eo5.nspname AS exp_oprschema5, "
+		"       eo1.oprname AS exp_oprname1, "
+		"       eo2.oprname AS exp_oprname2, "
+		"       eo3.oprname AS exp_oprname3, "
+		"       eo4.oprname AS exp_oprname4, "
+		"       eo5.oprname AS exp_oprname5, "
+		"       coalesce(io1.oid, 0) AS staop1, "
+		"       coalesce(io2.oid, 0) AS staop2, "
+		"       coalesce(io3.oid, 0) AS staop3, "
+		"       coalesce(io4.oid, 0) AS staop4, "
+		"       coalesce(io5.oid, 0) AS staop5, "
+		"       ec1.nspname AS exp_collschema1, "
+		"       ec2.nspname AS exp_collschema2, "
+		"       ec3.nspname AS exp_collschema3, "
+		"       ec4.nspname AS exp_collschema4, "
+		"       ec5.nspname AS exp_collschema5, "
+		"       ec1.collname AS exp_collname1, "
+		"       ec2.collname AS exp_collname2, "
+		"       ec3.collname AS exp_collname3, "
+		"       ec4.collname AS exp_collname4, "
+		"       ec5.collname AS exp_collname5, "
+		"       coalesce(ic1.oid, 0) AS stacoll1, "
+		"       coalesce(ic2.oid, 0) AS stacoll2, "
+		"       coalesce(ic3.oid, 0) AS stacoll3, "
+		"       coalesce(ic4.oid, 0) AS stacoll4, "
+		"       coalesce(ic5.oid, 0) AS stacoll5, "
+		"       (pga.attname IS DISTINCT FROM ea.attname) AS attname_miss, "
+		"       (ea.attnum IS DISTINCT FROM es.staattnum) AS staattnum_miss, "
+		"       COUNT(*) OVER (PARTITION BY pga.attnum, "
+		"                                   es.stainherit) AS dup_count "
+		"FROM pg_attribute AS pga "
+		"JOIN pg_type AS pgat ON pgat.oid = pga.atttypid "
+		"LEFT JOIN pg_collation AS pgac ON pgac.oid = pga.attcollation "
+		"LEFT JOIN exported_attributes AS ea ON ea.attname = pga.attname "
+		"LEFT JOIN exported_statistics AS es ON es.staattnum = ea.attnum "
+		"LEFT JOIN exported_types AS et ON et.oid = ea.atttypid "
+		"LEFT JOIN exported_collations AS ec ON ec.oid = ea.attcollation "
+		"LEFT JOIN exported_operators AS eo1 ON eo1.oid = es.staop1 "
+		"LEFT JOIN exported_operators AS eo2 ON eo2.oid = es.staop2 "
+		"LEFT JOIN exported_operators AS eo3 ON eo3.oid = es.staop3 "
+		"LEFT JOIN exported_operators AS eo4 ON eo4.oid = es.staop4 "
+		"LEFT JOIN exported_operators AS eo5 ON eo5.oid = es.staop5 "
+		"LEFT JOIN exported_collations AS ec1 ON ec1.oid = es.stacoll1 "
+		"LEFT JOIN exported_collations AS ec2 ON ec2.oid = es.stacoll2 "
+		"LEFT JOIN exported_collations AS ec3 ON ec3.oid = es.stacoll3 "
+		"LEFT JOIN exported_collations AS ec4 ON ec4.oid = es.stacoll4 "
+		"LEFT JOIN exported_collations AS ec5 ON ec5.oid = es.stacoll5 "
+		"LEFT JOIN pg_namespace AS ion1 ON ion1.nspname = eo1.nspname "
+		"LEFT JOIN pg_namespace AS ion2 ON ion2.nspname = eo2.nspname "
+		"LEFT JOIN pg_namespace AS ion3 ON ion3.nspname = eo3.nspname "
+		"LEFT JOIN pg_namespace AS ion4 ON ion4.nspname = eo4.nspname "
+		"LEFT JOIN pg_namespace AS ion5 ON ion5.nspname = eo5.nspname "
+		"LEFT JOIN pg_namespace AS icn1 ON icn1.nspname = ec1.nspname "
+		"LEFT JOIN pg_namespace AS icn2 ON icn2.nspname = ec2.nspname "
+		"LEFT JOIN pg_namespace AS icn3 ON icn3.nspname = ec3.nspname "
+		"LEFT JOIN pg_namespace AS icn4 ON icn4.nspname = ec4.nspname "
+		"LEFT JOIN pg_namespace AS icn5 ON icn5.nspname = ec5.nspname "
+		"LEFT JOIN pg_operator AS io1 ON io1.oprnamespace = ion1.oid "
+		"    AND io1.oprname = eo1.oprname "
+		"    AND io1.oprleft = pga.atttypid "
+		"    AND io1.oprright = pga.atttypid "
+		"LEFT JOIN pg_operator AS io2 ON io2.oprnamespace = ion2.oid "
+		"    AND io2.oprname = eo2.oprname "
+		"    AND io2.oprleft = pga.atttypid "
+		"    AND io2.oprright = pga.atttypid "
+		"LEFT JOIN pg_operator AS io3 ON io3.oprnamespace = ion3.oid "
+		"    AND io3.oprname = eo3.oprname "
+		"    AND io3.oprleft = pga.atttypid "
+		"    AND io3.oprright = pga.atttypid "
+		"LEFT JOIN pg_operator AS io4 ON io4.oprnamespace = ion4.oid "
+		"    AND io4.oprname = eo4.oprname "
+		"    AND io4.oprleft = pga.atttypid "
+		"    AND io4.oprright = pga.atttypid "
+		"LEFT JOIN pg_operator AS io5 ON io5.oprnamespace = ion5.oid "
+		"    AND io5.oprname = eo5.oprname "
+		"    AND io5.oprleft = pga.atttypid "
+		"    AND io5.oprright = pga.atttypid "
+		"LEFT JOIN pg_collation as ic1 "
+		"   ON ic1.collnamespace = icn1.oid AND ic1.collname = ec1.collname "
+		"LEFT JOIN pg_collation as ic2 "
+		"   ON ic2.collnamespace = icn2.oid AND ic2.collname = ec2.collname "
+		"LEFT JOIN pg_collation as ic3 "
+		"   ON ic3.collnamespace = icn3.oid AND ic3.collname = ec3.collname "
+		"LEFT JOIN pg_collation as ic4 "
+		"   ON ic4.collnamespace = icn4.oid AND ic4.collname = ec4.collname "
+		"LEFT JOIN pg_collation as ic5 "
+		"   ON ic5.collnamespace = icn5.oid AND ic5.collname = ec5.collname "
+		"WHERE pga.attrelid = $6 "
+		"AND pga.attnum > 0 "
+		"ORDER BY pga.attnum, coalesce(es.stainherit, false)";
+
+	/*
+	 * Columns with names containing _EXP_ are values that come from exported
+	 * json data and therefore should not be directly imported into
+	 * pg_statistic. Those values were joined to current catalog values to
+	 * derive the proper value to import, and the column is exposed mostly
+	 * for validation purposes.
+	 */
+	enum
+	{
+		PGS_ATTNUM = 0,
+		PGS_ATTNAME,
+		PGS_ATTTYPID,
+		PGS_ATTTYPMOD,
+		PGS_ATTCOLLATION,
+		PGS_TYPNAME,
+		PGS_COLLNAME,
+		PGS_EXP_ATTNUM,
+		PGS_EXP_ATTTYPID,
+		PGS_EXP_ATTCOLLATION,
+		PGS_EXP_TYPNAME,
+		PGS_EXP_TYPSCHEMA,
+		PGS_EXP_COLLNAME,
+		PGS_EXP_COLLSCHEMA,
+		PGS_STAINHERIT,
+		PGS_STANULLFRAC,
+		PGS_STAWIDTH,
+		PGS_STADISTINCT,
+		PGS_STAKIND1,
+		PGS_STAKIND2,
+		PGS_STAKIND3,
+		PGS_STAKIND4,
+		PGS_STAKIND5,
+		PGS_EXP_STAOP1,
+		PGS_EXP_STAOP2,
+		PGS_EXP_STAOP3,
+		PGS_EXP_STAOP4,
+		PGS_EXP_STAOP5,
+		PGS_EXP_STACOLL1,
+		PGS_EXP_STACOLL2,
+		PGS_EXP_STACOLL3,
+		PGS_EXP_STACOLL4,
+		PGS_EXP_STACOLL5,
+		PGS_STANUMBERS1,
+		PGS_STANUMBERS2,
+		PGS_STANUMBERS3,
+		PGS_STANUMBERS4,
+		PGS_STANUMBERS5,
+		PGS_STAVALUES1,
+		PGS_STAVALUES2,
+		PGS_STAVALUES3,
+		PGS_STAVALUES4,
+		PGS_STAVALUES5,
+		PGS_EXP_OPRSCHEMA1,
+		PGS_EXP_OPRSCHEMA2,
+		PGS_EXP_OPRSCHEMA3,
+		PGS_EXP_OPRSCHEMA4,
+		PGS_EXP_OPRSCHEMA5,
+		PGS_EXP_OPRNAME1,
+		PGS_EXP_OPRNAME2,
+		PGS_EXP_OPRNAME3,
+		PGS_EXP_OPRNAME4,
+		PGS_EXP_OPRNAME5,
+		PGS_STAOP1,
+		PGS_STAOP2,
+		PGS_STAOP3,
+		PGS_STAOP4,
+		PGS_STAOP5,
+		PGS_EXP_COLLSCHEMA1,
+		PGS_EXP_COLLSCHEMA2,
+		PGS_EXP_COLLSCHEMA3,
+		PGS_EXP_COLLSCHEMA4,
+		PGS_EXP_COLLSCHEMA5,
+		PGS_EXP_COLLNAME1,
+		PGS_EXP_COLLNAME2,
+		PGS_EXP_COLLNAME3,
+		PGS_EXP_COLLNAME4,
+		PGS_EXP_COLLNAME5,
+		PGS_STACOLL1,
+		PGS_STACOLL2,
+		PGS_STACOLL3,
+		PGS_STACOLL4,
+		PGS_STACOLL5,
+		PGS_ATTNAME_MISS,
+		PGS_STAATTNUM_MISS,
+		PGS_DUP_COUNT,
+		NUM_PGS_COLS
+	};
+
+	AttrInfo	*relattrinfo = get_attrinfo(rel);
+	AttrInfo	*attrinfo;
+
+	int		ret;
+	int		i;
+	int		tupctr = 0;
+
+	SPITupleTable  *tuptable;
+	HeapTuple	   *rettuples;
+
+	ret = SPI_execute_with_args(sql, PGS_NARGS, argtypes, args, argnulls, true, 0);
+
+	if (ret != SPI_OK_SELECT)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("statistic export JSON is not in proper format")));
+
+	tuptable = SPI_tuptable;
+
+	rettuples = palloc0(sizeof(HeapTuple) * tuptable->numvals);
+
+	for (i = 0; i < tuptable->numvals; i++)
+	{
+		Datum		pgs_datums[NUM_PGS_COLS];
+		bool		pgs_nulls[NUM_PGS_COLS];
+		bool		skip = false;
+
+		Datum		values[Natts_pg_statistic] = { 0 };
+		bool		nulls[Natts_pg_statistic] = { false };
+
+		int			dup_count;
+		AttrNumber	attnum;
+		char	   *attname;
+		bool		stainherit;
+		char	   *inhstr;
+		AttrNumber	exported_attnum;
+		FmgrInfo	finfo;
+		int			k;
+
+		heap_deform_tuple(tuptable->vals[i], tuptable->tupdesc, pgs_datums,
+						  pgs_nulls);
+
+		/*
+		 * Check all the columns that cannot plausibly be null regardless of
+		 * json data quality
+		 */
+		Assert(!pgs_nulls[PGS_ATTNUM]);
+		Assert(!pgs_nulls[PGS_ATTNAME]);
+		Assert(!pgs_nulls[PGS_ATTTYPID]);
+		Assert(!pgs_nulls[PGS_ATTTYPMOD]);
+		Assert(!pgs_nulls[PGS_ATTCOLLATION]);
+		Assert(!pgs_nulls[PGS_TYPNAME]);
+		Assert(!pgs_nulls[PGS_DUP_COUNT]);
+		Assert(!pgs_nulls[PGS_ATTNAME_MISS]);
+		Assert(!pgs_nulls[PGS_STAATTNUM_MISS]);
+
+		attnum = DatumGetInt16(pgs_datums[PGS_ATTNUM]);
+		attname = NameStr(*(DatumGetName(pgs_datums[PGS_ATTNAME])));
+		attrinfo = &relattrinfo[attnum - 1];
+
+		fmgr_info(F_ARRAY_IN, &finfo);
+
+		if (pgs_nulls[PGS_STAINHERIT])
+		{
+			stainherit = false;
+			inhstr = "NULL";
+		}
+		else if (DatumGetBool(pgs_datums[PGS_STAINHERIT]))
+		{
+			stainherit = true;
+			inhstr = "true";
+		}
+		else
+		{
+			stainherit = false;
+			inhstr = "false";
+		}
+
+		/*
+		 * Any duplicates would be a cache collision and a sign that the
+		 * import json is broken.
+		 */
+		dup_count = DatumGetInt32(pgs_datums[PGS_DUP_COUNT]);
+		if (dup_count != 1)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Attribute duplicate count %d on attnum %d attname %s stainherit %s",
+							dup_count, attnum, attname, stainherit ? "t" : "f")));
+		else if (DatumGetBool(pgs_datums[PGS_ATTNAME_MISS]))
+		{
+			/* Do not generate a tuple */
+			skip = true;
+			if (require_match_oids)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("No exported attribute with name \"%s\" found.", attname)));
+		}
+		else if (DatumGetBool(pgs_datums[PGS_STAATTNUM_MISS]))
+		{
+			/* Do not generate a tuple */
+			skip = true;
+			if (require_match_oids)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("No exported statistic found for exported attribute \"%s\" found.",
+								attname)));
+		}
+
+		/* if we are going to skip this row, clean up first */
+		if (skip)
+		{
+			pfree(attname);
+			continue;
+		}
+
+		exported_attnum = DatumGetInt16(pgs_datums[PGS_EXP_ATTNUM]);
+
+		if (require_match_oids)
+		{
+			Oid	export_typoid = DatumGetObjectId(pgs_datums[PGS_EXP_ATTTYPID]);
+			Oid	catalog_typoid = DatumGetObjectId(pgs_datums[PGS_ATTTYPID]);
+
+			if (export_typoid != catalog_typoid)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("Attribute %d expects typoid %u but typoid %u imported",
+								attnum, catalog_typoid, export_typoid)));
+		}
+
+		values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(RelationGetRelid(rel));
+		values[Anum_pg_statistic_staattnum - 1] = pgs_datums[PGS_ATTNUM];
+		values[Anum_pg_statistic_stainherit - 1] = BoolGetDatum(stainherit);
+
+		/*
+		 * Any nulls here will fail the when it is written to pg_statistic
+		 * but that error message is as good as any we could create.
+		 */
+		if (pgs_nulls[PGS_STANULLFRAC])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Imported Statistics Attribute %d stainherit %s cannot have NULL %s",
+							exported_attnum, inhstr, "stanullfrac")));
+
+		if (pgs_nulls[PGS_STAWIDTH])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Imported Statistics Attribute %d stainherit %s cannot have NULL %s",
+							exported_attnum, inhstr, "stawidth")));
+
+		if (pgs_nulls[PGS_STADISTINCT])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Imported Statistics Attribute %d stainherit %s cannot have NULL %s",
+							exported_attnum, inhstr, "stadistinct")));
+
+		values[Anum_pg_statistic_stanullfrac - 1] = pgs_datums[PGS_STANULLFRAC];
+		values[Anum_pg_statistic_stawidth - 1] = pgs_datums[PGS_STAWIDTH];
+		values[Anum_pg_statistic_stadistinct - 1] = pgs_datums[PGS_STADISTINCT];
+
+		for (k = 0; k < STATISTIC_NUM_SLOTS; k++)
+		{
+			int16	kind;
+			Oid		op;
+
+			/*
+			 * stakindN
+			 *
+			 * We can't match order of stakinds from VacAttrStats because which
+			 * entries appear varies by the data in the table.
+			 *
+			 * The stakindN values assigned during ANALYZE will vary by the
+			 * amount and quality of the data sampled. As such, there is no
+			 * fixed set of kinds to match against for any one slot.
+			 *
+			 * Any NULL stakindN values will cause the row to fail.
+			 *
+			 */
+			if (pgs_nulls[PGS_STAKIND1 + k])
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("Imported Statistics Attribute %d stainherit %s cannot have NULL %s%d",
+								exported_attnum, inhstr, "stakind", k+1)));
+
+			values[Anum_pg_statistic_stakind1 - 1 + k] = pgs_datums[PGS_STAKIND1 + k];
+			kind = DatumGetInt16(pgs_datums[PGS_STAKIND1 + k]);
+
+			/*
+			 * staopN
+			 *
+			 * We cannot resolve the exported operator back to a local Oid because
+			 * that cannot be looked up directly in the catalog, so we have to
+			 * instead look at the exported operator name, choose the op from
+			 * the typecache, and then if we're requiring matching oids we can
+			 * compare that to the exported oid.
+			 *
+			 */
+			/* Possibly validate operator must be OidIsValid when stakindN <> 0 */
+			if (pgs_nulls[PGS_EXP_OPRNAME1 + k])
+				op = InvalidOid;
+			else
+			{
+				char   *exp_oprname;
+
+				exp_oprname = TextDatumGetCString(pgs_datums[PGS_EXP_OPRNAME1 + k]);
+				if (strcmp(exp_oprname, "=") == 0)
+				{
+					/*
+					 * MCELEM stat arrays are of the same type as the
+					 * array base element type and are eqopr
+					 */
+					if ((kind == STATISTIC_KIND_MCELEM) ||
+						(kind == STATISTIC_KIND_DECHIST))
+						op = attrinfo->baseeqopr;
+					else
+						op = attrinfo->eqopr;
+				}
+				else if (strcmp(exp_oprname, "<") == 0)
+					op = attrinfo->ltopr;
+				else
+					op = InvalidOid;
+				pfree(exp_oprname);
+			}
+
+			if (require_match_oids)
+			{
+				if (pgs_nulls[PGS_EXP_STAOP1 + k])
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("Attribute %d staop%d kind %d expects Oid %u but NULL imported",
+									attnum, k+1, kind,  op)));
+				else
+				{
+					Oid	export_op = DatumGetObjectId(pgs_datums[PGS_EXP_STAOP1 + k]);
+					if (export_op != op)
+						ereport(ERROR,
+								(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+								 errmsg("Attribute %d staop%d kind %d expects Oid %u but Oid %u imported",
+										attnum, k+1, kind,  op, export_op)));
+				}
+			}
+			values[Anum_pg_statistic_staop1 - 1 + k] = ObjectIdGetDatum(op);
+
+			/* Any NULL stacollN will fail the row */
+			if (pgs_nulls[PGS_STACOLL1 + k])
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("Imported Statistics Attribute %d stainherit %s cannot have NULL %s%d",
+								exported_attnum, inhstr, "stacoll", k+1)));
+			values[Anum_pg_statistic_stacoll1 - 1 + k] = pgs_datums[PGS_STACOLL1 + k];
+
+			if (require_match_oids)
+			{
+				Oid	export_coll = DatumGetObjectId(pgs_datums[PGS_EXP_STACOLL1 + k]);
+				Oid	import_coll = DatumGetObjectId(pgs_datums[PGS_STACOLL1 + k]);
+
+				if (export_coll != import_coll)
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("Attribute %d stacoll%d expects Oid %u but Oid %u imported",
+									attnum, k+1, export_coll, import_coll)));
+			}
+
+			/* stanumbersN - the import query did the required type coercion. */
+			values[Anum_pg_statistic_stanumbers1 - 1 + k] =
+				pgs_datums[PGS_STANUMBERS1 + k];
+			nulls[Anum_pg_statistic_stanumbers1 - 1 + k] =
+				pgs_nulls[PGS_STANUMBERS1 + k];
+
+			/* stavaluesN */
+			if (pgs_nulls[PGS_STAVALUES1 + k])
+			{
+				nulls[Anum_pg_statistic_stavalues1 - 1 + k] = true;
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = (Datum) 0;
+			}
+			else
+			{
+				char    *s = TextDatumGetCString(pgs_datums[PGS_STAVALUES1 + k]);
+
+				values[Anum_pg_statistic_stavalues1 - 1 + k] =
+					FunctionCall3(&finfo, CStringGetDatum(s),
+								  ObjectIdGetDatum(attrinfo->basetypid),
+								  Int32GetDatum(attrinfo->typmod));
+
+				pfree(s);
+			}
+		}
+
+		/* Add valid tuple to the list */
+		rettuples[tupctr++] = heap_form_tuple(RelationGetDescr(sd), values, nulls);
+	}
+
+	pfree(relattrinfo);
+	*ntuples = tupctr;
+	return rettuples;
+}
+
+/*
+ * Import statistics for a given relation.
+ *
+ * The statistics json format is:
+ *
+ * {
+ *   "server_version_num": number, -- SHOW server_version on source system
+ *   "relname": string, -- pgclass.relname of the exported relation
+ *   "nspname": string, -- schema name for the exported relation
+ *   "reltuples": number, -- pg_class.reltuples
+ *   "relpages": number, -- pg_class.relpages
+ *   "types": [
+ *         -- export of all pg_type referenced in this json doc
+ *         {
+ *            "oid": number, -- pg_type.oid
+ *            "typname": string, -- pg_type.typname
+ *            "nspname": string -- schema name for the pg_type
+ *         }
+ *      ],
+ *   "collations": [
+ *         -- export all pg_collation reference in this json doc
+ *         {
+ *            "oid": number, -- pg_collation.oid
+ *            "collname": string, -- pg_collation.collname
+ *            "nspname": string -- schema name for the pg_collation
+ *         }
+ *      ],
+ *   "operators": [
+ *         -- export all pg_operator reference in this json doc
+ *         {
+ *            "oid": number, -- pg_operator.oid
+ *            "collname": string, -- pg_oprname
+ *            "nspname": string -- schema name for the pg_operator
+ *         }
+ *      ],
+ *   "attributes": [
+ *         -- export all pg_attribute for the exported relation
+ *         {
+ *            "attnum": number, -- pg_attribute.attnum
+ *            "attname": string, -- pg_attribute.attname
+ *            "atttypid": number, -- pg_attribute.atttypid
+ *            "attcollation": number -- pg_attribute.attcollation
+ *         }
+ *      ],
+ *   "statistics": [
+ *         -- export all pg_statistic for the exported relation
+ *         {
+ *            "staattnum": number, -- pg_statistic.staattnum
+ *            "stainherit": bool, -- pg_statistic.stainherit
+ *            "stanullfrac": number, -- pg_statistic.stanullfrac
+ *            "stawidth": number, -- pg_statistic.stawidth
+ *            "stadistinct": number, -- pg_statistic.stadistinct
+ *            "stakind1": number, -- pg_statistic.stakind1
+ *            "stakind2": number, -- pg_statistic.stakind2
+ *            "stakind3": number, -- pg_statistic.stakind3
+ *            "stakind4": number, -- pg_statistic.stakind4
+ *            "stakind5": number, -- pg_statistic.stakind5
+ *            "staop1": number, -- pg_statistic.staop1
+ *            "staop2": number, -- pg_statistic.staop2
+ *            "staop3": number, -- pg_statistic.staop3
+ *            "staop4": number, -- pg_statistic.staop4
+ *            "staop5": number, -- pg_statistic.staop5
+ *            "stacoll1": number, -- pg_statistic.stacoll1
+ *            "stacoll2": number, -- pg_statistic.stacoll2
+ *            "stacoll3": number, -- pg_statistic.stacoll3
+ *            "stacoll4": number, -- pg_statistic.stacoll4
+ *            "stacoll5": number, -- pg_statistic.stacoll5
+ *            -- stanumbersN are cast to string to aid array_in()
+ *            "stanumbers1": string, -- pg_statistic.stanumbers1::text
+ *            "stanumbers2": string, -- pg_statistic.stanumbers2::text
+ *            "stanumbers3": string, -- pg_statistic.stanumbers3::text
+ *            "stanumbers4": string, -- pg_statistic.stanumbers4::text
+ *            "stanumbers5": string, -- pg_statistic.stanumbers5::text
+ *            -- stavaluesN are cast to string to aid array_in()
+ *            "stavalues1": string, -- pg_statistic.stavalues1::text
+ *            "stavalues2": string, -- pg_statistic.stavalues2::text
+ *            "stavalues3": string, -- pg_statistic.stavalues3::text
+ *            "stavalues4": string, -- pg_statistic.stavalues4::text
+ *            "stavalues5": string -- pg_statistic.stavalues5::text
+ *         }
+ *      ]
+ *  }
+ *
+ * Each server verion exports a subset of this format. The exported format
+ * can and will change with each new version, and this function will have
+ * to account for those variations.
+
+ */
+Datum
+pg_import_rel_stats(PG_FUNCTION_ARGS)
+{
+	Oid		relid;
+	bool	validate;
+	bool	require_match_oids;
+
+	const char *sql =
+		"SELECT current_setting('server_version_num') AS current_version, eb.* "
+		"FROM jsonb_to_record($1) AS eb( "
+		"    server_version_num integer, "
+		"    relname text, "
+		"    nspname text, "
+		"    reltuples float4,"
+		"    relpages int4, "
+		"    types jsonb, "
+		"    collations jsonb, "
+		"    operators jsonb, "
+		"    attributes jsonb, "
+		"    statistics jsonb) ";
+
+	enum
+	{
+		BQ_CURRENT_VERSION_NUM = 0,
+		BQ_SERVER_VERSION_NUM,
+		BQ_RELNAME,
+		BQ_NSPNAME,
+		BQ_RELTUPLES,
+		BQ_RELPAGES,
+		BQ_TYPES,
+		BQ_COLLATIONS,
+		BQ_OPERATORS,
+		BQ_ATTRIBUTES,
+		BQ_STATISTICS,
+		NUM_BQ_COLS
+	};
+
+#define BQ_NARGS 1
+
+	Oid			argtypes[BQ_NARGS] = { JSONBOID };
+	Datum		args[BQ_NARGS];
+
+	int		ret;
+
+	SPITupleTable  *tuptable;
+
+	Datum	datums[NUM_BQ_COLS];
+	bool	nulls[NUM_BQ_COLS];
+
+	int32	server_version_num;
+	int32	current_version_num;
+
+	Relation	rel;
+	Relation	sd;
+	HeapTuple  *sdtuples;
+	int			nsdtuples;
+	int			i;
+
+	CatalogIndexState	indstate = NULL;
+
+	if (PG_ARGISNULL(0))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relation cannot be NULL")));
+	relid = PG_GETARG_OID(0);
+
+	if (PG_ARGISNULL(1))
+		PG_RETURN_BOOL(false);
+	args[0] = PG_GETARG_DATUM(1);
+
+	if (PG_ARGISNULL(2))
+		validate = false;
+	else
+		validate = PG_GETARG_BOOL(2);
+
+	if (PG_ARGISNULL(3))
+		require_match_oids = false;
+	else
+		require_match_oids = PG_GETARG_BOOL(3);
+
+	/*
+	 * Connect to SPI manager
+	 */
+	if ((ret = SPI_connect()) < 0)
+		elog(ERROR, "SPI connect failure - returned %d", ret);
+
+	/*
+	 * Fetch the base level of the stats json. The results found there will
+	 * determine how the nested data will be handled.
+	 */
+	ret = SPI_execute_with_args(sql, BQ_NARGS, argtypes, args, NULL, true, 1);
+
+	/*
+	 * Only allow one qualifying tuple
+	 */
+	if (ret != SPI_OK_SELECT)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("statistic export JSON is not in proper format")));
+
+	if (SPI_processed > 1)
+		ereport(ERROR,
+				(errcode(ERRCODE_CARDINALITY_VIOLATION),
+				 errmsg("statistic export JSON should return only one base object")));
+
+	tuptable = SPI_tuptable;
+	heap_deform_tuple(tuptable->vals[0], tuptable->tupdesc, datums, nulls);
+
+	/*
+	 * Check for valid combination of exported server_version_num to the local
+	 * server_version_num. We won't be reusing these values in a query so use
+	 * scratch datum/null vars.
+	 */
+	if (nulls[BQ_CURRENT_VERSION_NUM])
+		ereport(ERROR,
+				(errcode(ERRCODE_SQL_JSON_ITEM_CANNOT_BE_CAST_TO_TARGET_TYPE),
+				 errmsg("current_version_num cannot be null")));
+
+	if (nulls[BQ_SERVER_VERSION_NUM])
+		ereport(ERROR,
+				(errcode(ERRCODE_SQL_JSON_ITEM_CANNOT_BE_CAST_TO_TARGET_TYPE),
+				 errmsg("server_version_num cannot be null")));
+
+	current_version_num = DatumGetInt32(datums[BQ_CURRENT_VERSION_NUM]);
+	server_version_num = DatumGetInt32(datums[BQ_SERVER_VERSION_NUM]);
+
+	if (server_version_num <= 100000)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("Cannot import statistics from servers below version 10.0")));
+
+	if (server_version_num > current_version_num)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("Cannot import statistics from server version %d to %d",
+						server_version_num, current_version_num)));
+
+	rel = relation_open(relid, ShareUpdateExclusiveLock);
+
+	if (require_match_oids)
+	{
+		char   *curr_relname = SPI_getrelname(rel);
+		char   *curr_nspname = SPI_getnspname(rel);
+		char   *import_relname;
+		char   *import_nspname;
+
+		if (nulls[BQ_RELNAME])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Imported Relation Name must match relation name, but is null")));
+
+		if (nulls[BQ_NSPNAME])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Imported Relation Schema Name must match schema name, but is null")));
+
+		import_relname = TextDatumGetCString(datums[BQ_RELNAME]);
+		import_nspname = TextDatumGetCString(datums[BQ_NSPNAME]);
+
+		if (strcmp(import_relname, curr_relname) != 0)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Imported Relation Name (%s) must match relation name (%s), but does not",
+							import_relname, curr_relname)));
+
+		if (strcmp(import_nspname, curr_nspname) != 0)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Imported Relation Schema Name (%s) must match schema name (%s), but does not",
+							import_nspname, curr_nspname)));
+
+		pfree(curr_relname);
+		pfree(curr_nspname);
+		pfree(import_relname);
+		pfree(import_nspname);
+	}
+
+	/*
+	 * validations
+	 *
+	 * Potential future validations:
+	 *
+	 *  * all attributes.atttypid values are represented in "types"
+	 *  * all attributes.attcollation values are represented in "types"
+	 *  * attributes.attname is of acceptable length
+	 *  * all non-invalid statistics.opN values are represented in "operators"
+	 *  * all non-invalid statistics.collN values are represented in "collations"
+	 *  * statistincs.kindN values in 0-7
+	 *  * statistics.stanullfrac in range
+	 *  * statistics.stawidth in range
+	 *  * statistics.ndistinct in rage
+	 *
+	 */
+	if (validate)
+	{
+		validate_exported_types(datums[BQ_TYPES], nulls[BQ_TYPES]);
+		validate_exported_collations(datums[BQ_COLLATIONS], nulls[BQ_COLLATIONS]);
+		validate_exported_operators(datums[BQ_OPERATORS], nulls[BQ_OPERATORS]);
+		validate_exported_attributes(datums[BQ_ATTRIBUTES], nulls[BQ_ATTRIBUTES]);
+		validate_exported_statistics(datums[BQ_STATISTICS], nulls[BQ_STATISTICS]);
+	}
+
+	sd = table_open(StatisticRelationId, RowExclusiveLock);
+
+	sdtuples = import_pg_statistics(rel, sd, server_version_num,
+									&datums[BQ_TYPES], &nulls[BQ_TYPES],
+									require_match_oids, &nsdtuples);
+
+	/* Open index information when we know we need it */
+	indstate = CatalogOpenIndexes(sd);
+
+	/* Delete existing pg_statistic rows for relation to avoid collisions */
+	remove_pg_statistics(rel, sd, false);
+	if (RELKIND_HAS_PARTITIONS(rel->rd_rel->relkind))
+		remove_pg_statistics(rel, sd, true);
+
+	for (i = 0; i < nsdtuples; i++)
+	{
+		CatalogTupleInsertWithInfo(sd, sdtuples[i], indstate);
+		heap_freetuple(sdtuples[i]);
+	}
+
+	CatalogCloseIndexes(indstate);
+	table_close(sd, RowExclusiveLock);
+	pfree(sdtuples);
+
+	/*
+	 * Update pg_class tuple directly (non-transactionally, same as
+	 * is done in do_analyze().
+	 *
+	 * Only modify pg_class row if changes are to be made
+	 */
+	if (!nulls[BQ_RELTUPLES] || !nulls[BQ_RELPAGES])
+	{
+		Relation		pg_class_rel;
+		HeapTuple		ctup;
+		Form_pg_class	pgcform;
+
+		/*
+		 * Open the relation, getting ShareUpdateExclusiveLock to ensure that no
+		 * other stat-setting operation can run on it concurrently.
+		 */
+		pg_class_rel = table_open(RelationRelationId, ShareUpdateExclusiveLock);
+
+		/* leave if relation could not be opened or locked */
+		if (!pg_class_rel)
+			PG_RETURN_BOOL(false);
+
+		ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid));
+		if (!HeapTupleIsValid(ctup))
+			elog(ERROR, "pg_class entry for relid %u vanished during statistics import",
+				 relid);
+
+		pgcform = (Form_pg_class) GETSTRUCT(ctup);
+
+		/* leave un-set values alone */
+		if (!nulls[BQ_RELTUPLES])
+			pgcform->reltuples = DatumGetFloat4(datums[BQ_RELTUPLES]);
+
+		if(!nulls[BQ_RELPAGES])
+			pgcform->relpages = DatumGetInt32(datums[BQ_RELPAGES]);
+
+		heap_inplace_update(pg_class_rel, ctup);
+		table_close(pg_class_rel, ShareUpdateExclusiveLock);
+	}
+
+	relation_close(rel, NoLock);
+
+	SPI_finish();
+
+	PG_RETURN_BOOL(true);
+}
diff --git a/src/test/regress/expected/stats_export_import.out b/src/test/regress/expected/stats_export_import.out
new file mode 100644
index 0000000000..5ab51c5aa0
--- /dev/null
+++ b/src/test/regress/expected/stats_export_import.out
@@ -0,0 +1,530 @@
+-- set to 't' to see debug output
+\set debug f
+CREATE SCHEMA stats_export_import;
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b float,
+    c text,
+    d date,
+    e jsonb);
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    tags text[]
+);
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type, array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, NULL;
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+-- Capture pg_statistic values for table and index
+CREATE TABLE stats_export_import.pg_statistic_capture
+AS
+SELECT  starelid,
+        staattnum, stainherit, stanullfrac, stawidth, stadistinct,
+        stakind1, stakind2, stakind3, stakind4, stakind5,
+        staop1, staop2, staop3, staop4, staop5, stacoll1, stacoll2, stacoll3, stacoll4, stacoll5,
+        stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic
+WHERE starelid IN ('stats_export_import.test'::regclass,
+                    'stats_export_import.is_odd'::regclass);
+SELECT COUNT(*)
+FROM stats_export_import.pg_statistic_capture;
+ count 
+-------
+     5
+(1 row)
+
+-- Export stats
+SELECT
+    jsonb_build_object(
+        'server_version_num', current_setting('server_version_num'),
+        'relname', r.relname,
+        'nspname', n.nspname,
+        'reltuples', r.reltuples,
+        'relpages', r.relpages,
+        'types',
+        (
+            SELECT array_agg(tr ORDER BY tr.oid)
+            FROM (
+                SELECT
+                    t.oid,
+                    t.typname,
+                    n.nspname
+                FROM pg_type AS t
+                JOIN pg_namespace AS n ON n.oid = t.typnamespace
+                WHERE t.oid IN (
+                    SELECT a.atttypid
+                    FROM pg_attribute AS a
+                    WHERE a.attrelid = r.oid
+                    AND NOT a.attisdropped
+                    AND a.attnum > 0
+                )
+            ) AS tr
+        ),
+        'collations',
+        (
+            SELECT array_agg(cr ORDER BY cr.oid)
+            FROM (
+                SELECT
+                    c.oid,
+                    c.collname,
+                    n.nspname
+                FROM pg_collation AS c
+                JOIN pg_namespace AS n ON n.oid = c.collnamespace
+                WHERE c.oid IN (
+                    SELECT a.attcollation AS oid
+                    FROM pg_attribute AS a
+                    WHERE a.attrelid = r.oid
+                    AND NOT a.attisdropped
+                    AND a.attnum > 0
+                    UNION
+                    SELECT u.collid
+                    FROM pg_statistic AS s
+                    CROSS JOIN LATERAL unnest(ARRAY[
+                                        s.stacoll1, s.stacoll2,
+                                        s.stacoll3, s.stacoll4,
+                                        s.stacoll5]) AS u(collid)
+                    WHERE s.starelid = r.oid
+                    )
+                ) AS cr
+        ),
+        'operators',
+        (
+            SELECT array_agg(p ORDER BY p.oid)
+            FROM (
+                SELECT
+                    o.oid,
+                    o.oprname,
+                    n.nspname
+                FROM pg_operator AS o
+                JOIN pg_namespace AS n ON n.oid = o.oprnamespace
+                WHERE o.oid IN (
+                    SELECT u.oid
+                    FROM pg_statistic AS s
+                    CROSS JOIN LATERAL unnest(ARRAY[
+                                        s.staop1, s.staop2,
+                                        s.staop3, s.staop4,
+                                        s.staop5]) AS u(opid)
+                    WHERE s.starelid = r.oid
+                    )
+            ) AS p
+        ),
+        'attributes',
+        (
+            SELECT array_agg(ar ORDER BY ar.attnum)
+            FROM (
+                SELECT
+                    a.attnum,
+                    a.attname,
+                    a.atttypid,
+                    a.attcollation
+                FROM pg_attribute AS a
+                WHERE a.attrelid = r.oid
+                AND NOT a.attisdropped
+                AND a.attnum > 0
+            ) AS ar
+        ),
+        'statistics',
+        (
+            SELECT array_agg(sr ORDER BY sr.stainherit, sr.staattnum)
+            FROM (
+                SELECT
+                     s.staattnum,
+                     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::text AS stanumbers1,
+                     s.stanumbers2::text AS stanumbers2,
+                     s.stanumbers3::text AS stanumbers3,
+                     s.stanumbers4::text AS stanumbers4,
+                     s.stanumbers5::text AS stanumbers5,
+                     s.stavalues1::text AS stavalues1,
+                     s.stavalues2::text AS stavalues2,
+                     s.stavalues3::text AS stavalues3,
+                     s.stavalues4::text AS stavalues4,
+                     s.stavalues5::text AS stavalues5
+                FROM pg_statistic AS s
+                WHERE s.starelid = r.oid
+            ) AS sr
+        )
+    ) AS table_stats_json
+FROM pg_class AS r
+JOIN pg_namespace AS n ON n.oid = r.relnamespace
+WHERE r.oid = 'stats_export_import.test'::regclass
+\gset
+SELECT jsonb_pretty(:'table_stats_json'::jsonb) AS table_stats_json
+WHERE :'debug'::boolean;
+ table_stats_json 
+------------------
+(0 rows)
+
+SELECT
+    jsonb_build_object(
+        'server_version_num', current_setting('server_version_num'),
+        'relname', r.relname,
+        'nspname', n.nspname,
+        'reltuples', r.reltuples,
+        'relpages', r.relpages,
+        'types',
+        (
+            SELECT array_agg(tr ORDER BY tr.oid)
+            FROM (
+                SELECT
+                    t.oid,
+                    t.typname,
+                    n.nspname
+                FROM pg_type AS t
+                JOIN pg_namespace AS n ON n.oid = t.typnamespace
+                WHERE t.oid IN (
+                    SELECT a.atttypid
+                    FROM pg_attribute AS a
+                    WHERE a.attrelid = r.oid
+                    AND NOT a.attisdropped
+                    AND a.attnum > 0
+                )
+            ) AS tr
+        ),
+        'collations',
+        (
+            SELECT array_agg(cr ORDER BY cr.oid)
+            FROM (
+                SELECT
+                    c.oid,
+                    c.collname,
+                    n.nspname
+                FROM pg_collation AS c
+                JOIN pg_namespace AS n ON n.oid = c.collnamespace
+                WHERE c.oid IN (
+                    SELECT a.attcollation AS oid
+                    FROM pg_attribute AS a
+                    WHERE a.attrelid = r.oid
+                    AND NOT a.attisdropped
+                    AND a.attnum > 0
+                    UNION
+                    SELECT u.collid
+                    FROM pg_statistic AS s
+                    CROSS JOIN LATERAL unnest(ARRAY[
+                                        s.stacoll1, s.stacoll2,
+                                        s.stacoll3, s.stacoll4,
+                                        s.stacoll5]) AS u(collid)
+                    WHERE s.starelid = r.oid
+                    )
+                ) AS cr
+        ),
+        'operators',
+        (
+            SELECT array_agg(p ORDER BY p.oid)
+            FROM (
+                SELECT
+                    o.oid,
+                    o.oprname,
+                    n.nspname
+                FROM pg_operator AS o
+                JOIN pg_namespace AS n ON n.oid = o.oprnamespace
+                WHERE o.oid IN (
+                    SELECT u.oid
+                    FROM pg_statistic AS s
+                    CROSS JOIN LATERAL unnest(ARRAY[
+                                        s.staop1, s.staop2,
+                                        s.staop3, s.staop4,
+                                        s.staop5]) AS u(opid)
+                    WHERE s.starelid = r.oid
+                    )
+            ) AS p
+        ),
+        'attributes',
+        (
+            SELECT array_agg(ar ORDER BY ar.attnum)
+            FROM (
+                SELECT
+                    a.attnum,
+                    a.attname,
+                    a.atttypid,
+                    a.attcollation
+                FROM pg_attribute AS a
+                WHERE a.attrelid = r.oid
+                AND NOT a.attisdropped
+                AND a.attnum > 0
+            ) AS ar
+        ),
+        'statistics',
+        (
+            SELECT array_agg(sr ORDER BY sr.stainherit, sr.staattnum)
+            FROM (
+                SELECT
+                     s.staattnum,
+                     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::text AS stanumbers1,
+                     s.stanumbers2::text AS stanumbers2,
+                     s.stanumbers3::text AS stanumbers3,
+                     s.stanumbers4::text AS stanumbers4,
+                     s.stanumbers5::text AS stanumbers5,
+                     s.stavalues1::text AS stavalues1,
+                     s.stavalues2::text AS stavalues2,
+                     s.stavalues3::text AS stavalues3,
+                     s.stavalues4::text AS stavalues4,
+                     s.stavalues5::text AS stavalues5
+                FROM pg_statistic AS s
+                WHERE s.starelid = r.oid
+            ) AS sr
+        )
+    ) AS index_stats_json
+FROM pg_class AS r
+JOIN pg_namespace AS n ON n.oid = r.relnamespace
+WHERE r.oid = 'stats_export_import.is_odd'::regclass
+\gset
+SELECT jsonb_pretty(:'index_stats_json'::jsonb) AS index_stats_json
+WHERE :'debug'::boolean;
+ index_stats_json 
+------------------
+(0 rows)
+
+SELECT relname, reltuples
+FROM pg_class
+WHERE oid IN ('stats_export_import.test'::regclass,
+              'stats_export_import.is_odd'::regclass)
+ORDER BY relname;
+ relname | reltuples 
+---------+-----------
+ is_odd  |         4
+ test    |         4
+(2 rows)
+
+-- Move table and index out of the way
+ALTER TABLE stats_export_import.test RENAME TO test_orig;
+ALTER INDEX stats_export_import.is_odd RENAME TO is_odd_orig;
+-- Create empty copy tables
+CREATE TABLE stats_export_import.test(LIKE stats_export_import.test_orig);
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+-- Verify no stats for these new tables
+SELECT COUNT(*)
+FROM pg_statistic
+WHERE starelid IN('stats_export_import.test'::regclass,
+              'stats_export_import.is_odd'::regclass);
+ count 
+-------
+     0
+(1 row)
+
+SELECT relname, reltuples
+FROM pg_class
+WHERE oid IN ('stats_export_import.test'::regclass,
+              'stats_export_import.is_odd'::regclass)
+ORDER BY relname;
+ relname | reltuples 
+---------+-----------
+ is_odd  |         0
+ test    |        -1
+(2 rows)
+
+-- Test valiation
+SELECT
+    jsonb_build_object(
+        'server_version_num', current_setting('server_version_num'),
+        'relname', 'test',
+        'nspname', 'stats_export_import',
+        'types',
+        (
+            SELECT array_agg(r)
+            FROM (
+                SELECT 1 AS oid, 'type1' AS typname
+                UNION ALL
+                SELECT 2 AS oid, 'type2' AS typname
+                UNION ALL
+                SELECT 2 AS oid, 'type3' AS typname
+                ) AS r
+        )) AS invalid_types_doc,
+    jsonb_build_object(
+        'server_version_num', current_setting('server_version_num'),
+        'relname', 'test',
+        'nspname', 'stats_export_import',
+        'collations',
+        (
+            SELECT array_agg(r)
+            FROM (
+                SELECT 1 AS oid, 'coll1' AS collname
+                UNION ALL
+                SELECT 1 AS oid, 'coll2' AS collname
+                UNION ALL
+                SELECT 2 AS oid, 'coll3' AS collname
+                ) AS r
+        )) AS invalid_collations_doc,
+    jsonb_build_object(
+        'server_version_num', current_setting('server_version_num'),
+        'relname', 'test',
+        'nspname', 'stats_export_import',
+        'operators',
+        (
+            SELECT array_agg(r)
+            FROM (
+                SELECT 1 AS oid, 'opr1' AS oprname
+                UNION ALL
+                SELECT 3 AS oid, 'opr2' AS oprname
+                UNION ALL
+                SELECT 3 AS oid, 'opr3' AS oprname
+                ) AS r
+        )) AS invalid_operators_doc,
+    jsonb_build_object(
+        'server_version_num', current_setting('server_version_num'),
+        'relname', 'test',
+        'nspname', 'stats_export_import',
+        'attributes',
+        (
+            SELECT array_agg(r)
+            FROM (
+                SELECT 1 AS attnum, 'col1' AS attname
+                UNION ALL
+                SELECT 4 AS attnum, 'col2' AS attname
+                UNION ALL
+                SELECT 4 AS attnum, 'col3' AS attname
+                ) AS r
+        )) AS invalid_attributes_doc,
+    jsonb_build_object(
+        'server_version_num', current_setting('server_version_num'),
+        'relname', 'test',
+        'nspname', 'stats_export_import',
+        'statistics',
+        (
+            SELECT array_agg(r)
+            FROM (
+                SELECT 1 AS staattnum, false AS stainherit
+                UNION ALL
+                SELECT 5 AS staattnum, true AS stainherit
+                UNION ALL
+                SELECT 1 AS staattnum, false AS stainherit
+                ) AS r
+        )) AS invalid_statistics_doc
+\gset
+SELECT pg_import_rel_stats('stats_export_import.test'::regclass,
+        :'invalid_types_doc'::jsonb, true, true);
+ERROR:  statistic export JSON document "types" has duplicate rows with oid = 2
+SELECT pg_import_rel_stats('stats_export_import.test'::regclass,
+        :'invalid_collations_doc'::jsonb, true, true);
+ERROR:  statistic export JSON document "collations" has duplicate rows with oid = 1
+SELECT pg_import_rel_stats('stats_export_import.test'::regclass,
+        :'invalid_operators_doc'::jsonb, true, true);
+ERROR:  statistic export JSON document "operators" has duplicate rows with oid = 3
+SELECT pg_import_rel_stats('stats_export_import.test'::regclass,
+        :'invalid_attributes_doc'::jsonb, true, true);
+ERROR:  statistic export JSON document "attributes" has duplicate rows with attnum = 4
+SELECT pg_import_rel_stats('stats_export_import.test'::regclass,
+        :'invalid_statistics_doc'::jsonb, true, true);
+ERROR:  statistic export JSON document "statistics" has duplicate rows with staattnum = 1, stainherit = f
+-- Import stats
+SELECT pg_import_rel_stats(
+        'stats_export_import.test'::regclass,
+        :'table_stats_json'::jsonb,
+        true,
+        true);
+ pg_import_rel_stats 
+---------------------
+ t
+(1 row)
+
+SELECT pg_import_rel_stats(
+        'stats_export_import.is_odd'::regclass,
+        :'index_stats_json'::jsonb,
+        true,
+        true);
+ pg_import_rel_stats 
+---------------------
+ t
+(1 row)
+
+-- This should return 0 rows
+SELECT staattnum, 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
+FROM stats_export_import.pg_statistic_capture
+EXCEPT
+SELECT staattnum, stainherit, stanullfrac, stawidth, stadistinct,
+        stakind1, stakind2, stakind3, stakind4, stakind5,
+        staop1, staop2, staop3, staop4, staop5, stacoll1, stacoll2, stacoll3, stacoll4, stacoll5,
+        stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic
+WHERE starelid IN ('stats_export_import.test'::regclass,
+                    'stats_export_import.is_odd'::regclass);
+ staattnum | 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 
+-----------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----
+(0 rows)
+
+-- This should return 0 rows
+SELECT staattnum, stainherit, stanullfrac, stawidth, stadistinct,
+        stakind1, stakind2, stakind3, stakind4, stakind5,
+        staop1, staop2, staop3, staop4, staop5, stacoll1, stacoll2, stacoll3, stacoll4, stacoll5,
+        stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic
+WHERE starelid IN ('stats_export_import.test'::regclass,
+                    'stats_export_import.is_odd'::regclass)
+EXCEPT
+SELECT staattnum, 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
+FROM stats_export_import.pg_statistic_capture;
+ staattnum | 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 
+-----------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----
+(0 rows)
+
+SELECT relname, reltuples
+FROM pg_class
+WHERE oid IN ('stats_export_import.test'::regclass,
+              'stats_export_import.is_odd'::regclass)
+ORDER BY relname;
+ relname | reltuples 
+---------+-----------
+ is_odd  |         4
+ test    |         4
+(2 rows)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 1d8a414eea..0c89ffc02d 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -103,7 +103,7 @@ test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combo
 # ----------
 # Another group of parallel tests (JSON related)
 # ----------
-test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson
+test: json jsonb json_encoding jsonpath jsonpath_encoding jsonb_jsonpath sqljson stats_export_import
 
 # ----------
 # Another group of parallel tests
diff --git a/src/test/regress/sql/stats_export_import.sql b/src/test/regress/sql/stats_export_import.sql
new file mode 100644
index 0000000000..9a80eebeec
--- /dev/null
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -0,0 +1,499 @@
+-- set to 't' to see debug output
+\set debug f
+CREATE SCHEMA stats_export_import;
+
+CREATE TYPE stats_export_import.complex_type AS (
+    a integer,
+    b float,
+    c text,
+    d date,
+    e jsonb);
+
+CREATE TABLE stats_export_import.test(
+    id INTEGER PRIMARY KEY,
+    name text,
+    comp stats_export_import.complex_type,
+    tags text[]
+);
+
+INSERT INTO stats_export_import.test
+SELECT 1, 'one', (1, 1.1, 'ONE', '2001-01-01', '{ "xkey": "xval" }')::stats_export_import.complex_type, array['red','green']
+UNION ALL
+SELECT 2, 'two', (2, 2.2, 'TWO', '2002-02-02', '[true, 4, "six"]')::stats_export_import.complex_type, array['blue','yellow']
+UNION ALL
+SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.complex_type, array['"orange"', 'purple', 'cyan']
+UNION ALL
+SELECT 4, 'four', NULL, NULL;
+
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+
+-- Generate statistics on table with data
+ANALYZE stats_export_import.test;
+
+-- Capture pg_statistic values for table and index
+CREATE TABLE stats_export_import.pg_statistic_capture
+AS
+SELECT  starelid,
+        staattnum, stainherit, stanullfrac, stawidth, stadistinct,
+        stakind1, stakind2, stakind3, stakind4, stakind5,
+        staop1, staop2, staop3, staop4, staop5, stacoll1, stacoll2, stacoll3, stacoll4, stacoll5,
+        stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic
+WHERE starelid IN ('stats_export_import.test'::regclass,
+                    'stats_export_import.is_odd'::regclass);
+
+SELECT COUNT(*)
+FROM stats_export_import.pg_statistic_capture;
+
+-- Export stats
+SELECT
+    jsonb_build_object(
+        'server_version_num', current_setting('server_version_num'),
+        'relname', r.relname,
+        'nspname', n.nspname,
+        'reltuples', r.reltuples,
+        'relpages', r.relpages,
+        'types',
+        (
+            SELECT array_agg(tr ORDER BY tr.oid)
+            FROM (
+                SELECT
+                    t.oid,
+                    t.typname,
+                    n.nspname
+                FROM pg_type AS t
+                JOIN pg_namespace AS n ON n.oid = t.typnamespace
+                WHERE t.oid IN (
+                    SELECT a.atttypid
+                    FROM pg_attribute AS a
+                    WHERE a.attrelid = r.oid
+                    AND NOT a.attisdropped
+                    AND a.attnum > 0
+                )
+            ) AS tr
+        ),
+        'collations',
+        (
+            SELECT array_agg(cr ORDER BY cr.oid)
+            FROM (
+                SELECT
+                    c.oid,
+                    c.collname,
+                    n.nspname
+                FROM pg_collation AS c
+                JOIN pg_namespace AS n ON n.oid = c.collnamespace
+                WHERE c.oid IN (
+                    SELECT a.attcollation AS oid
+                    FROM pg_attribute AS a
+                    WHERE a.attrelid = r.oid
+                    AND NOT a.attisdropped
+                    AND a.attnum > 0
+                    UNION
+                    SELECT u.collid
+                    FROM pg_statistic AS s
+                    CROSS JOIN LATERAL unnest(ARRAY[
+                                        s.stacoll1, s.stacoll2,
+                                        s.stacoll3, s.stacoll4,
+                                        s.stacoll5]) AS u(collid)
+                    WHERE s.starelid = r.oid
+                    )
+                ) AS cr
+        ),
+        'operators',
+        (
+            SELECT array_agg(p ORDER BY p.oid)
+            FROM (
+                SELECT
+                    o.oid,
+                    o.oprname,
+                    n.nspname
+                FROM pg_operator AS o
+                JOIN pg_namespace AS n ON n.oid = o.oprnamespace
+                WHERE o.oid IN (
+                    SELECT u.oid
+                    FROM pg_statistic AS s
+                    CROSS JOIN LATERAL unnest(ARRAY[
+                                        s.staop1, s.staop2,
+                                        s.staop3, s.staop4,
+                                        s.staop5]) AS u(opid)
+                    WHERE s.starelid = r.oid
+                    )
+            ) AS p
+        ),
+        'attributes',
+        (
+            SELECT array_agg(ar ORDER BY ar.attnum)
+            FROM (
+                SELECT
+                    a.attnum,
+                    a.attname,
+                    a.atttypid,
+                    a.attcollation
+                FROM pg_attribute AS a
+                WHERE a.attrelid = r.oid
+                AND NOT a.attisdropped
+                AND a.attnum > 0
+            ) AS ar
+        ),
+        'statistics',
+        (
+            SELECT array_agg(sr ORDER BY sr.stainherit, sr.staattnum)
+            FROM (
+                SELECT
+                     s.staattnum,
+                     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::text AS stanumbers1,
+                     s.stanumbers2::text AS stanumbers2,
+                     s.stanumbers3::text AS stanumbers3,
+                     s.stanumbers4::text AS stanumbers4,
+                     s.stanumbers5::text AS stanumbers5,
+                     s.stavalues1::text AS stavalues1,
+                     s.stavalues2::text AS stavalues2,
+                     s.stavalues3::text AS stavalues3,
+                     s.stavalues4::text AS stavalues4,
+                     s.stavalues5::text AS stavalues5
+                FROM pg_statistic AS s
+                WHERE s.starelid = r.oid
+            ) AS sr
+        )
+    ) AS table_stats_json
+FROM pg_class AS r
+JOIN pg_namespace AS n ON n.oid = r.relnamespace
+WHERE r.oid = 'stats_export_import.test'::regclass
+\gset
+
+SELECT jsonb_pretty(:'table_stats_json'::jsonb) AS table_stats_json
+WHERE :'debug'::boolean;
+
+SELECT
+    jsonb_build_object(
+        'server_version_num', current_setting('server_version_num'),
+        'relname', r.relname,
+        'nspname', n.nspname,
+        'reltuples', r.reltuples,
+        'relpages', r.relpages,
+        'types',
+        (
+            SELECT array_agg(tr ORDER BY tr.oid)
+            FROM (
+                SELECT
+                    t.oid,
+                    t.typname,
+                    n.nspname
+                FROM pg_type AS t
+                JOIN pg_namespace AS n ON n.oid = t.typnamespace
+                WHERE t.oid IN (
+                    SELECT a.atttypid
+                    FROM pg_attribute AS a
+                    WHERE a.attrelid = r.oid
+                    AND NOT a.attisdropped
+                    AND a.attnum > 0
+                )
+            ) AS tr
+        ),
+        'collations',
+        (
+            SELECT array_agg(cr ORDER BY cr.oid)
+            FROM (
+                SELECT
+                    c.oid,
+                    c.collname,
+                    n.nspname
+                FROM pg_collation AS c
+                JOIN pg_namespace AS n ON n.oid = c.collnamespace
+                WHERE c.oid IN (
+                    SELECT a.attcollation AS oid
+                    FROM pg_attribute AS a
+                    WHERE a.attrelid = r.oid
+                    AND NOT a.attisdropped
+                    AND a.attnum > 0
+                    UNION
+                    SELECT u.collid
+                    FROM pg_statistic AS s
+                    CROSS JOIN LATERAL unnest(ARRAY[
+                                        s.stacoll1, s.stacoll2,
+                                        s.stacoll3, s.stacoll4,
+                                        s.stacoll5]) AS u(collid)
+                    WHERE s.starelid = r.oid
+                    )
+                ) AS cr
+        ),
+        'operators',
+        (
+            SELECT array_agg(p ORDER BY p.oid)
+            FROM (
+                SELECT
+                    o.oid,
+                    o.oprname,
+                    n.nspname
+                FROM pg_operator AS o
+                JOIN pg_namespace AS n ON n.oid = o.oprnamespace
+                WHERE o.oid IN (
+                    SELECT u.oid
+                    FROM pg_statistic AS s
+                    CROSS JOIN LATERAL unnest(ARRAY[
+                                        s.staop1, s.staop2,
+                                        s.staop3, s.staop4,
+                                        s.staop5]) AS u(opid)
+                    WHERE s.starelid = r.oid
+                    )
+            ) AS p
+        ),
+        'attributes',
+        (
+            SELECT array_agg(ar ORDER BY ar.attnum)
+            FROM (
+                SELECT
+                    a.attnum,
+                    a.attname,
+                    a.atttypid,
+                    a.attcollation
+                FROM pg_attribute AS a
+                WHERE a.attrelid = r.oid
+                AND NOT a.attisdropped
+                AND a.attnum > 0
+            ) AS ar
+        ),
+        'statistics',
+        (
+            SELECT array_agg(sr ORDER BY sr.stainherit, sr.staattnum)
+            FROM (
+                SELECT
+                     s.staattnum,
+                     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::text AS stanumbers1,
+                     s.stanumbers2::text AS stanumbers2,
+                     s.stanumbers3::text AS stanumbers3,
+                     s.stanumbers4::text AS stanumbers4,
+                     s.stanumbers5::text AS stanumbers5,
+                     s.stavalues1::text AS stavalues1,
+                     s.stavalues2::text AS stavalues2,
+                     s.stavalues3::text AS stavalues3,
+                     s.stavalues4::text AS stavalues4,
+                     s.stavalues5::text AS stavalues5
+                FROM pg_statistic AS s
+                WHERE s.starelid = r.oid
+            ) AS sr
+        )
+    ) AS index_stats_json
+FROM pg_class AS r
+JOIN pg_namespace AS n ON n.oid = r.relnamespace
+WHERE r.oid = 'stats_export_import.is_odd'::regclass
+\gset
+
+SELECT jsonb_pretty(:'index_stats_json'::jsonb) AS index_stats_json
+WHERE :'debug'::boolean;
+
+SELECT relname, reltuples
+FROM pg_class
+WHERE oid IN ('stats_export_import.test'::regclass,
+              'stats_export_import.is_odd'::regclass)
+ORDER BY relname;
+
+-- Move table and index out of the way
+ALTER TABLE stats_export_import.test RENAME TO test_orig;
+ALTER INDEX stats_export_import.is_odd RENAME TO is_odd_orig;
+
+-- Create empty copy tables
+CREATE TABLE stats_export_import.test(LIKE stats_export_import.test_orig);
+CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+
+-- Verify no stats for these new tables
+SELECT COUNT(*)
+FROM pg_statistic
+WHERE starelid IN('stats_export_import.test'::regclass,
+              'stats_export_import.is_odd'::regclass);
+
+SELECT relname, reltuples
+FROM pg_class
+WHERE oid IN ('stats_export_import.test'::regclass,
+              'stats_export_import.is_odd'::regclass)
+ORDER BY relname;
+
+
+-- Test valiation
+SELECT
+    jsonb_build_object(
+        'server_version_num', current_setting('server_version_num'),
+        'relname', 'test',
+        'nspname', 'stats_export_import',
+        'types',
+        (
+            SELECT array_agg(r)
+            FROM (
+                SELECT 1 AS oid, 'type1' AS typname
+                UNION ALL
+                SELECT 2 AS oid, 'type2' AS typname
+                UNION ALL
+                SELECT 2 AS oid, 'type3' AS typname
+                ) AS r
+        )) AS invalid_types_doc,
+    jsonb_build_object(
+        'server_version_num', current_setting('server_version_num'),
+        'relname', 'test',
+        'nspname', 'stats_export_import',
+        'collations',
+        (
+            SELECT array_agg(r)
+            FROM (
+                SELECT 1 AS oid, 'coll1' AS collname
+                UNION ALL
+                SELECT 1 AS oid, 'coll2' AS collname
+                UNION ALL
+                SELECT 2 AS oid, 'coll3' AS collname
+                ) AS r
+        )) AS invalid_collations_doc,
+    jsonb_build_object(
+        'server_version_num', current_setting('server_version_num'),
+        'relname', 'test',
+        'nspname', 'stats_export_import',
+        'operators',
+        (
+            SELECT array_agg(r)
+            FROM (
+                SELECT 1 AS oid, 'opr1' AS oprname
+                UNION ALL
+                SELECT 3 AS oid, 'opr2' AS oprname
+                UNION ALL
+                SELECT 3 AS oid, 'opr3' AS oprname
+                ) AS r
+        )) AS invalid_operators_doc,
+    jsonb_build_object(
+        'server_version_num', current_setting('server_version_num'),
+        'relname', 'test',
+        'nspname', 'stats_export_import',
+        'attributes',
+        (
+            SELECT array_agg(r)
+            FROM (
+                SELECT 1 AS attnum, 'col1' AS attname
+                UNION ALL
+                SELECT 4 AS attnum, 'col2' AS attname
+                UNION ALL
+                SELECT 4 AS attnum, 'col3' AS attname
+                ) AS r
+        )) AS invalid_attributes_doc,
+    jsonb_build_object(
+        'server_version_num', current_setting('server_version_num'),
+        'relname', 'test',
+        'nspname', 'stats_export_import',
+        'statistics',
+        (
+            SELECT array_agg(r)
+            FROM (
+                SELECT 1 AS staattnum, false AS stainherit
+                UNION ALL
+                SELECT 5 AS staattnum, true AS stainherit
+                UNION ALL
+                SELECT 1 AS staattnum, false AS stainherit
+                ) AS r
+        )) AS invalid_statistics_doc
+\gset
+
+SELECT pg_import_rel_stats('stats_export_import.test'::regclass,
+        :'invalid_types_doc'::jsonb, true, true);
+
+SELECT pg_import_rel_stats('stats_export_import.test'::regclass,
+        :'invalid_collations_doc'::jsonb, true, true);
+
+SELECT pg_import_rel_stats('stats_export_import.test'::regclass,
+        :'invalid_operators_doc'::jsonb, true, true);
+
+SELECT pg_import_rel_stats('stats_export_import.test'::regclass,
+        :'invalid_attributes_doc'::jsonb, true, true);
+
+SELECT pg_import_rel_stats('stats_export_import.test'::regclass,
+        :'invalid_statistics_doc'::jsonb, true, true);
+
+-- Import stats
+SELECT pg_import_rel_stats(
+        'stats_export_import.test'::regclass,
+        :'table_stats_json'::jsonb,
+        true,
+        true);
+
+SELECT pg_import_rel_stats(
+        'stats_export_import.is_odd'::regclass,
+        :'index_stats_json'::jsonb,
+        true,
+        true);
+
+-- This should return 0 rows
+SELECT staattnum, 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
+FROM stats_export_import.pg_statistic_capture
+EXCEPT
+SELECT staattnum, stainherit, stanullfrac, stawidth, stadistinct,
+        stakind1, stakind2, stakind3, stakind4, stakind5,
+        staop1, staop2, staop3, staop4, staop5, stacoll1, stacoll2, stacoll3, stacoll4, stacoll5,
+        stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic
+WHERE starelid IN ('stats_export_import.test'::regclass,
+                    'stats_export_import.is_odd'::regclass);
+
+-- This should return 0 rows
+SELECT staattnum, stainherit, stanullfrac, stawidth, stadistinct,
+        stakind1, stakind2, stakind3, stakind4, stakind5,
+        staop1, staop2, staop3, staop4, staop5, stacoll1, stacoll2, stacoll3, stacoll4, stacoll5,
+        stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic
+WHERE starelid IN ('stats_export_import.test'::regclass,
+                    'stats_export_import.is_odd'::regclass)
+EXCEPT
+SELECT staattnum, 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
+FROM stats_export_import.pg_statistic_capture;
+
+SELECT relname, reltuples
+FROM pg_class
+WHERE oid IN ('stats_export_import.test'::regclass,
+              'stats_export_import.is_odd'::regclass)
+ORDER BY relname;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 6788ba8ef4..d16983dff3 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -28689,6 +28689,62 @@ 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_import_rel_stats</primary>
+        </indexterm>
+        <function>pg_import_rel_stats</function> ( <parameter>relation</parameter> <type>regclass</type>, <parameter>relation_stats</parameter> <type>jsonb</type>, <parameter>validate</parameter> <type>bool</type>, <parameter>require_match_oids</parameter> <type>bool</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Modifies the <structname>pg_class</structname> row with the
+        <structfield>oid</structfield> matching <parameter>relation</parameter>
+        to set the <structfield>reltuples</structfield> and
+        <structfield>relpages</structfield> fields. This is done nontransactionally.
+        The <structname>pg_statistic</structname> rows for the
+        <structfield>statrelid</structfield> matching <parameter>relation</parameter>
+        are replaced with the values found in <parameter>relation_stats</parameter>,
+        and this is done transactionally. The purpose of this function is to apply
+        statistics values in an upgrade situation that are "good enough" for system
+        operation until they are replaced by the next auto-analyze. This function
+        could be 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>
+        If <parameter>validate</parameter> is set to <literal>true</literal>,
+        then the function will perform a series of data consistency checks on
+        the data in <parameter>relation_stats</parameter> before attempting to
+        import statistics. Any inconsistencies found will raise an error.
+       </para>
+       <para>
+        If <parameter>require_match_oids</parameter> is set to <literal>true</literal>,
+        then the import will fail if the imported oids for <structname>pt_type</structname>,
+        <structname>pg_collation</structname>, and <structname>pg_operator</structname> do
+        not match the values specified in <parameter>relation_json</parameter>, as would be expected
+        in a binary upgrade. These assumptions would not be true when restoring from a dump.
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
-- 
2.43.0

From a9de415586cb1dd73c06c173100be9aa6ea47695 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Fri, 2 Feb 2024 02:59:50 -0500
Subject: [PATCH v4 2/3] This is the extended statistics equivalent of
 pg_import_rel_stats().

The most likely application of this function is to quickly apply stats
to a newly upgraded database faster than could be accomplished by
vacuumdb --analyze-in-stages.

The exported values stored in the parameter extended_stats are
compared against the existing structure in pg_statistic_ext and are
transformed into pg_statistic_ext_data rows, transactionally replacing
any pre-existing rows for that object.

The statistics applied are not locked in any way, and will be
overwritten by the next analyze, either explicit or via autovacuum.

This function also allows for tweaking of table statistics in-place,
allowing the user to simulate correlations, skew histograms, etc, to see
what those changes will evoke from the query planner.
---
 src/include/catalog/pg_proc.dat               |   5 +
 .../statistics/extended_stats_internal.h      |   7 +
 src/backend/statistics/dependencies.c         | 161 ++++
 src/backend/statistics/extended_stats.c       | 884 ++++++++++++++++--
 src/backend/statistics/mcv.c                  | 192 ++++
 src/backend/statistics/mvdistinct.c           | 160 ++++
 .../regress/expected/stats_export_import.out  | 265 +++++-
 src/test/regress/sql/stats_export_import.sql  | 245 ++++-
 doc/src/sgml/func.sgml                        |  28 +-
 9 files changed, 1874 insertions(+), 73 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index ec8ce7c3c0..2d12bb9b08 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6125,6 +6125,11 @@
 { oid => '9161', descr => 'adjust time to local time zone',
   proname => 'timezone', provolatile => 's', prorettype => 'timetz',
   proargtypes => 'timetz', prosrc => 'timetz_at_local' },
+{ oid => '9162',
+  descr => 'statistics: import to extended stats object',
+  proname => 'pg_import_ext_stats', provolatile => 'v', proisstrict => 't',
+  proparallel => 'u', prorettype => 'bool', proargtypes => 'oid jsonb bool bool',
+  prosrc => 'pg_import_ext_stats' },
 { oid => '2039', descr => 'hash',
   proname => 'timestamp_hash', prorettype => 'int4', proargtypes => 'timestamp',
   prosrc => 'timestamp_hash' },
diff --git a/src/include/statistics/extended_stats_internal.h b/src/include/statistics/extended_stats_internal.h
index 8eed9b338d..e325a76e63 100644
--- a/src/include/statistics/extended_stats_internal.h
+++ b/src/include/statistics/extended_stats_internal.h
@@ -70,15 +70,22 @@ typedef struct StatsBuildData

 extern MVNDistinct *statext_ndistinct_build(double totalrows, StatsBuildData *data);
+extern MVNDistinct *statext_ndistinct_import(Oid relid, Datum ndistinct,
+						bool ndististinct_null, Datum attributes,
+						bool attributes_null);
 extern bytea *statext_ndistinct_serialize(MVNDistinct *ndistinct);
 extern MVNDistinct *statext_ndistinct_deserialize(bytea *data);
 
 extern MVDependencies *statext_dependencies_build(StatsBuildData *data);
+extern MVDependencies *statext_dependencies_import(Oid relid,
+							Datum dependencies, bool dependencies_null,
+							Datum attributes, bool attributes_null);
 extern bytea *statext_dependencies_serialize(MVDependencies *dependencies);
 extern MVDependencies *statext_dependencies_deserialize(bytea *data);
 
 extern MCVList *statext_mcv_build(StatsBuildData *data,
 								  double totalrows, int stattarget);
+extern MCVList *statext_mcv_import(Datum mcv, bool mcv_null, VacAttrStats **stats);
 extern bytea *statext_mcv_serialize(MCVList *mcvlist, VacAttrStats **stats);
 extern MCVList *statext_mcv_deserialize(bytea *data);
 
diff --git a/src/backend/statistics/dependencies.c b/src/backend/statistics/dependencies.c
index 4752b99ed5..e482eca557 100644
--- a/src/backend/statistics/dependencies.c
+++ b/src/backend/statistics/dependencies.c
@@ -18,6 +18,7 @@
 #include "catalog/pg_operator.h"
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_statistic_ext_data.h"
+#include "executor/spi.h"
 #include "lib/stringinfo.h"
 #include "nodes/nodeFuncs.h"
 #include "nodes/nodes.h"
@@ -27,6 +28,7 @@
 #include "parser/parsetree.h"
 #include "statistics/extended_stats_internal.h"
 #include "statistics/statistics.h"
+#include "utils/builtins.h"
 #include "utils/bytea.h"
 #include "utils/fmgroids.h"
 #include "utils/fmgrprotos.h"
@@ -1829,3 +1831,162 @@ dependencies_clauselist_selectivity(PlannerInfo *root,
 
 	return s1;
 }
+
+/*
+ * statext_dependencies_import
+ *
+ * The dependencies serialization is a string that looks like
+ *       {"2 => 3": 0.258241, "1 => 2": 0.0, ...}
+ *
+ *   The integers represent attnums in the exported table, and these
+ *   may not line up with the attnums in the destination table so we
+ *   match them by name.
+ *
+ *   This structure can be coerced into JSON, but we must use JSON
+ *   over JSONB because JSON preserves key order and JSONB does not.
+ *
+ *
+ */
+MVDependencies *
+statext_dependencies_import(Oid relid,
+							Datum dependencies, bool dependencies_null,
+							Datum attributes, bool attributes_null)
+{
+	MVDependencies *result = NULL;
+
+#define DEPS_NARGS 3
+
+	Oid			argtypes[DEPS_NARGS] = { OIDOID, TEXTOID, JSONBOID };
+	Datum		args[DEPS_NARGS] = { relid, dependencies, attributes };
+	char		argnulls[DEPS_NARGS] = { ' ',
+					dependencies_null ? 'n' : ' ',
+					attributes_null ? 'n' : ' ' };
+
+	const char *sql =
+		"SELECT "
+		"    dep.depord, "
+		"    da.depattrord, "
+		"    da.exp_attnum, "
+		"    ea.attname AS exp_attname, "
+		"    CASE "
+		"        WHEN da.exp_attnum < 0 THEN da.exp_attnum "
+		"        ELSE pga.attnum "
+		"    END AS attnum, "
+		"    dep.degree::float8 AS degree, "
+		"    COUNT(*) OVER (PARTITION BY dep.depord) AS num_attrs, "
+		"    MAX(dep.depord) OVER () AS num_deps "
+		"FROM json_each_text($2::json) "
+		"     WITH ORDINALITY AS dep(attrs, degree, depord) "
+		"CROSS JOIN LATERAL unnest( string_to_array( "
+		"         replace(dep.attrs, ' => ', ', '), ', ')::int2[]) "
+		"     WITH ORDINALITY AS da(exp_attnum, depattrord) "
+		"LEFT JOIN LATERAL jsonb_to_recordset($3) AS ea(attnum int2, attname text) "
+		"     ON ea.attnum = da.exp_attnum AND da.exp_attnum > 0 "
+		"LEFT JOIN pg_attribute AS pga "
+		"     ON pga.attrelid = $1 AND pga.attname = ea.attname "
+		"ORDER BY dep.depord, da.depattrord ";
+
+	enum {
+		DEPS_DEPORD = 0,
+		DEPS_DEPATTRORD,
+		DEPS_EXP_ATTNUM,
+		DEPS_EXP_ATTNAME,
+		DEPS_ATTNUM,
+		DEPS_DEGREE,
+		DEPS_NUM_ATTRS,
+		DEPS_NUM_DEPS,
+		NUM_DEPS_COLS
+	};
+
+	SPITupleTable  *tuptable;
+	int				ret;
+	int				ndeps;
+	int				j = 0;
+
+	ret = SPI_execute_with_args(sql, DEPS_NARGS, argtypes, args, argnulls, true, 0);
+
+	if (ret != SPI_OK_SELECT)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("statistic export JSON is not in proper format")));
+
+	tuptable = SPI_tuptable;
+	if (tuptable->numvals == 0)
+		ndeps = 0;
+	else
+	{
+		bool isnull;
+		Datum d = SPI_getbinval(tuptable->vals[0], tuptable->tupdesc,
+								DEPS_NUM_DEPS+1, &isnull);
+		ndeps = DatumGetInt32(d);
+
+		if (isnull)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Indeterminate number of dependencies")));
+	}
+
+	if (ndeps == 0)
+		result = (MVDependencies *) palloc0(sizeof(MVDependencies));
+	else
+		result = (MVDependencies *) palloc0(offsetof(MVDependencies, deps)
+												   + (ndeps * sizeof(MVDependency *)));
+
+	result->magic = STATS_DEPS_MAGIC;
+	result->type = STATS_DEPS_TYPE_BASIC;
+	result->ndeps = ndeps;
+
+	for (j = 0; j < tuptable->numvals; j++)
+	{
+		Datum			datums[NUM_DEPS_COLS];
+		bool			nulls[NUM_DEPS_COLS];
+		int				natts;
+		int				d;
+		int				a;
+
+		heap_deform_tuple(tuptable->vals[j], tuptable->tupdesc, datums, nulls);
+
+		Assert(!nulls[DEPS_DEPORD]);
+		d = DatumGetInt32(datums[DEPS_DEPORD]) - 1;
+		Assert(!nulls[DEPS_DEPATTRORD]);
+		a = DatumGetInt32(datums[DEPS_DEPATTRORD]) - 1;
+
+		if (a == 0)
+		{
+			/* New MVDependnecy */
+			Assert(!nulls[DEPS_NUM_ATTRS]);
+			natts = DatumGetInt32(datums[DEPS_NUM_ATTRS]);
+
+			result->deps[d] = palloc0(offsetof(MVDependency, attributes)
+										+ (natts * sizeof(AttrNumber)));
+
+			result->deps[d]->nattributes = natts;
+			Assert(!nulls[DEPS_DEGREE]);
+			result->deps[d]->degree = DatumGetFloat8(datums[DEPS_DEGREE]);
+		}
+
+		if (!nulls[DEPS_ATTNUM])
+			result->deps[d]->attributes[a] = DatumGetInt16(datums[DEPS_ATTNUM]);
+		else if (nulls[DEPS_EXP_ATTNUM])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Dependency exported attnum cannot be null")));
+		else if (nulls[DEPS_ATTNUM])
+		{
+			AttrNumber exp_attnum = DatumGetInt16(datums[DEPS_EXP_ATTNUM]);
+
+			if (nulls[DEPS_EXP_ATTNAME])
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("Dependency has no exported name for attnum %d",
+								exp_attnum)));
+
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Dependency tried to match attnum %d by name (%s) but found no match",
+						 exp_attnum, TextDatumGetCString(datums[DEPS_EXP_ATTNAME]))));
+		}
+	}
+
+	return result;
+}
diff --git a/src/backend/statistics/extended_stats.c b/src/backend/statistics/extended_stats.c
index c5461514d8..76ae150c5b 100644
--- a/src/backend/statistics/extended_stats.c
+++ b/src/backend/statistics/extended_stats.c
@@ -19,12 +19,14 @@
 #include "access/detoast.h"
 #include "access/genam.h"
 #include "access/htup_details.h"
+#include "access/relation.h"
 #include "access/table.h"
 #include "catalog/indexing.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_statistic_ext_data.h"
 #include "executor/executor.h"
+#include "executor/spi.h"
 #include "commands/defrem.h"
 #include "commands/progress.h"
 #include "miscadmin.h"
@@ -32,6 +34,7 @@
 #include "optimizer/clauses.h"
 #include "optimizer/optimizer.h"
 #include "parser/parsetree.h"
+#include "parser/parse_oper.h"
 #include "pgstat.h"
 #include "postmaster/autovacuum.h"
 #include "statistics/extended_stats_internal.h"
@@ -418,6 +421,83 @@ statext_is_kind_built(HeapTuple htup, char type)
 	return !heap_attisnull(htup, attnum, NULL);
 }
 
+/*
+ * Create a single StatExtEntry from a fetched heap tuple
+ */
+static StatExtEntry *
+create_stat_ext_entry(HeapTuple htup)
+{
+	StatExtEntry *entry;
+	Datum		datum;
+	bool		isnull;
+	int			i;
+	ArrayType  *arr;
+	char	   *enabled;
+	Form_pg_statistic_ext staForm;
+	List	   *exprs = NIL;
+
+	entry = palloc0(sizeof(StatExtEntry));
+	staForm = (Form_pg_statistic_ext) GETSTRUCT(htup);
+	entry->statOid = staForm->oid;
+	entry->schema = get_namespace_name(staForm->stxnamespace);
+	entry->name = pstrdup(NameStr(staForm->stxname));
+	entry->stattarget = staForm->stxstattarget;
+	for (i = 0; i < staForm->stxkeys.dim1; i++)
+	{
+		entry->columns = bms_add_member(entry->columns,
+										staForm->stxkeys.values[i]);
+	}
+
+	/* decode the stxkind char array into a list of chars */
+	datum = SysCacheGetAttrNotNull(STATEXTOID, htup,
+								   Anum_pg_statistic_ext_stxkind);
+	arr = DatumGetArrayTypeP(datum);
+	if (ARR_NDIM(arr) != 1 ||
+		ARR_HASNULL(arr) ||
+		ARR_ELEMTYPE(arr) != CHAROID)
+		elog(ERROR, "stxkind is not a 1-D char array");
+	enabled = (char *) ARR_DATA_PTR(arr);
+	for (i = 0; i < ARR_DIMS(arr)[0]; i++)
+	{
+		Assert((enabled[i] == STATS_EXT_NDISTINCT) ||
+			   (enabled[i] == STATS_EXT_DEPENDENCIES) ||
+			   (enabled[i] == STATS_EXT_MCV) ||
+			   (enabled[i] == STATS_EXT_EXPRESSIONS));
+		entry->types = lappend_int(entry->types, (int) enabled[i]);
+	}
+
+	/* decode expression (if any) */
+	datum = SysCacheGetAttr(STATEXTOID, htup,
+							Anum_pg_statistic_ext_stxexprs, &isnull);
+
+	if (!isnull)
+	{
+		char	   *exprsString;
+
+		exprsString = TextDatumGetCString(datum);
+		exprs = (List *) stringToNode(exprsString);
+
+		pfree(exprsString);
+
+		/*
+		 * Run the expressions through eval_const_expressions. This is not
+		 * just an optimization, but is necessary, because the planner
+		 * will be comparing them to similarly-processed qual clauses, and
+		 * may fail to detect valid matches without this.  We must not use
+		 * canonicalize_qual, however, since these aren't qual
+		 * expressions.
+		 */
+		exprs = (List *) eval_const_expressions(NULL, (Node *) exprs);
+
+		/* May as well fix opfuncids too */
+		fix_opfuncids((Node *) exprs);
+	}
+
+	entry->exprs = exprs;
+
+	return entry;
+}
+
 /*
  * Return a list (of StatExtEntry) of statistics objects for the given relation.
  */
@@ -443,74 +523,7 @@ fetch_statentries_for_relation(Relation pg_statext, Oid relid)
 
 	while (HeapTupleIsValid(htup = systable_getnext(scan)))
 	{
-		StatExtEntry *entry;
-		Datum		datum;
-		bool		isnull;
-		int			i;
-		ArrayType  *arr;
-		char	   *enabled;
-		Form_pg_statistic_ext staForm;
-		List	   *exprs = NIL;
-
-		entry = palloc0(sizeof(StatExtEntry));
-		staForm = (Form_pg_statistic_ext) GETSTRUCT(htup);
-		entry->statOid = staForm->oid;
-		entry->schema = get_namespace_name(staForm->stxnamespace);
-		entry->name = pstrdup(NameStr(staForm->stxname));
-		entry->stattarget = staForm->stxstattarget;
-		for (i = 0; i < staForm->stxkeys.dim1; i++)
-		{
-			entry->columns = bms_add_member(entry->columns,
-											staForm->stxkeys.values[i]);
-		}
-
-		/* decode the stxkind char array into a list of chars */
-		datum = SysCacheGetAttrNotNull(STATEXTOID, htup,
-									   Anum_pg_statistic_ext_stxkind);
-		arr = DatumGetArrayTypeP(datum);
-		if (ARR_NDIM(arr) != 1 ||
-			ARR_HASNULL(arr) ||
-			ARR_ELEMTYPE(arr) != CHAROID)
-			elog(ERROR, "stxkind is not a 1-D char array");
-		enabled = (char *) ARR_DATA_PTR(arr);
-		for (i = 0; i < ARR_DIMS(arr)[0]; i++)
-		{
-			Assert((enabled[i] == STATS_EXT_NDISTINCT) ||
-				   (enabled[i] == STATS_EXT_DEPENDENCIES) ||
-				   (enabled[i] == STATS_EXT_MCV) ||
-				   (enabled[i] == STATS_EXT_EXPRESSIONS));
-			entry->types = lappend_int(entry->types, (int) enabled[i]);
-		}
-
-		/* decode expression (if any) */
-		datum = SysCacheGetAttr(STATEXTOID, htup,
-								Anum_pg_statistic_ext_stxexprs, &isnull);
-
-		if (!isnull)
-		{
-			char	   *exprsString;
-
-			exprsString = TextDatumGetCString(datum);
-			exprs = (List *) stringToNode(exprsString);
-
-			pfree(exprsString);
-
-			/*
-			 * Run the expressions through eval_const_expressions. This is not
-			 * just an optimization, but is necessary, because the planner
-			 * will be comparing them to similarly-processed qual clauses, and
-			 * may fail to detect valid matches without this.  We must not use
-			 * canonicalize_qual, however, since these aren't qual
-			 * expressions.
-			 */
-			exprs = (List *) eval_const_expressions(NULL, (Node *) exprs);
-
-			/* May as well fix opfuncids too */
-			fix_opfuncids((Node *) exprs);
-		}
-
-		entry->exprs = exprs;
-
+		StatExtEntry *entry = create_stat_ext_entry(htup);
 		result = lappend(result, entry);
 	}
 
@@ -2636,3 +2649,738 @@ make_build_data(Relation rel, StatExtEntry *stat, int numrows, HeapTuple *rows,
 
 	return result;
 }
+
+static Datum
+import_expressions(Datum stxdexpr, bool stxdexpr_null,
+				   Datum operators, bool operators_null,
+				   VacAttrStats **expr_stats, int nexprs)
+{
+
+#define EXPR_NARGS 2
+
+	Oid			argtypes[EXPR_NARGS] = { JSONBOID, JSONBOID };
+	Datum		args[EXPR_NARGS] = { stxdexpr, operators };
+	char		argnulls[EXPR_NARGS] = {
+					stxdexpr_null ? 'n' : ' ',
+					operators_null ? 'n' : ' ' };
+
+	const char *sql =
+		"WITH exported_operators AS ( "
+		"    SELECT eo.* "
+		"    FROM jsonb_to_recordset($2) "
+		"        AS eo(oid oid, oprname text) "
+		") "
+		"SELECT s.*, "
+		"       eo1.oprname AS eoprname1, "
+		"       eo2.oprname AS eoprname2, "
+		"       eo3.oprname AS eoprname3, "
+		"       eo4.oprname AS eoprname4, "
+		"       eo5.oprname AS eoprname5 "
+		"FROM jsonb_to_recordset($1) "
+		"    AS s(staattnum integer, "
+		"         stainherit boolean, "
+		"         stanullfrac float4, "
+		"         stawidth integer, "
+		"         stadistinct float4, "
+		"         stakind1 int2, "
+		"         stakind2 int2, "
+		"         stakind3 int2, "
+		"         stakind4 int2, "
+		"         stakind5 int2, "
+		"         staop1 oid, "
+		"         staop2 oid, "
+		"         staop3 oid, "
+		"         staop4 oid, "
+		"         staop5 oid, "
+		"         stacoll1 oid, "
+		"         stacoll2 oid, "
+		"         stacoll3 oid, "
+		"         stacoll4 oid, "
+		"         stacoll5 oid, "
+		"         stanumbers1 float4[], "
+		"         stanumbers2 float4[], "
+		"         stanumbers3 float4[], "
+		"         stanumbers4 float4[], "
+		"         stanumbers5 float4[], "
+		"         stavalues1 text, "
+		"         stavalues2 text, "
+		"         stavalues3 text, "
+		"         stavalues4 text, "
+		"         stavalues5 text) "
+		"LEFT JOIN exported_operators AS eo1 ON eo1.oid = s.staop1 "
+		"LEFT JOIN exported_operators AS eo2 ON eo2.oid = s.staop2 "
+		"LEFT JOIN exported_operators AS eo3 ON eo3.oid = s.staop3 "
+		"LEFT JOIN exported_operators AS eo4 ON eo4.oid = s.staop4 "
+		"LEFT JOIN exported_operators AS eo5 ON eo5.oid = s.staop5 ";
+
+	enum
+	{
+		EXPR_ATTNUM = 0,
+		EXPR_STAINHERIT,
+		EXPR_STANULLFRAC,
+		EXPR_STAWIDTH,
+		EXPR_STADISTINCT,
+		EXPR_STAKIND1,
+		EXPR_STAKIND2,
+		EXPR_STAKIND3,
+		EXPR_STAKIND4,
+		EXPR_STAKIND5,
+		EXPR_STAOP1,
+		EXPR_STAOP2,
+		EXPR_STAOP3,
+		EXPR_STAOP4,
+		EXPR_STAOP5,
+		EXPR_STACOLL1,
+		EXPR_STACOLL2,
+		EXPR_STACOLL3,
+		EXPR_STACOLL4,
+		EXPR_STACOLL5,
+		EXPR_STANUMBERS1,
+		EXPR_STANUMBERS2,
+		EXPR_STANUMBERS3,
+		EXPR_STANUMBERS4,
+		EXPR_STANUMBERS5,
+		EXPR_STAVALUES1,
+		EXPR_STAVALUES2,
+		EXPR_STAVALUES3,
+		EXPR_STAVALUES4,
+		EXPR_STAVALUES5,
+		EXPR_EOPRNAME1,
+		EXPR_EOPRNAME2,
+		EXPR_EOPRNAME3,
+		EXPR_EOPRNAME4,
+		EXPR_EOPRNAME5,
+		NUM_EXPR_COLS
+	};
+
+	SPITupleTable  *tuptable;
+	int				ret;
+	int				e;
+
+	ArrayBuildState *astate = NULL;
+
+	Relation	pgsd;
+	HeapTuple	pgstup;
+	Oid			pgstypoid;
+	FmgrInfo	finfo;
+
+	pgsd = table_open(StatisticRelationId, RowExclusiveLock);
+	pgstypoid = get_rel_type_id(StatisticRelationId);
+	fmgr_info(F_ARRAY_IN, &finfo);
+
+	if (!OidIsValid(pgstypoid))
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("relation \"%s\" does not have a composite type",
+						"pg_statistic")));
+
+	ret = SPI_execute_with_args(sql, EXPR_NARGS, argtypes, args, argnulls, true, 0);
+
+	if (ret != SPI_OK_SELECT)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("statistic export JSON is not in proper format")));
+
+	tuptable = SPI_tuptable;
+
+	if (nexprs != tuptable->numvals)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("statistic export expected %d stxdexpr rows but found %lu",
+					 nexprs, tuptable->numvals)));
+
+	if (nexprs == 0)
+		astate = accumArrayResult(astate,
+								  (Datum) 0,
+								  true,
+								  pgstypoid,
+								  CurrentMemoryContext);
+
+	for (e = 0; e < nexprs; e++)
+	{
+		Datum	values[Natts_pg_statistic] = { 0 };
+		bool	nulls[Natts_pg_statistic] = { false };
+
+		Datum	rs_datums[NUM_EXPR_COLS];
+		bool	rs_nulls[NUM_EXPR_COLS];
+
+		VacAttrStats   *stats = expr_stats[e];
+
+		Oid 	basetypoid;
+		Oid		ltopr;
+		Oid 	baseltopr;
+		Oid		eqopr;
+		Oid 	baseeqopr;
+		int 	k;
+
+		/*
+		 * If if the stat is an array, then we want the base element
+		 * type. This mimics the calculation in get_attrinfo().
+		 */
+		get_sort_group_operators(stats->attrtypid,
+								 false, false, false,
+								 &ltopr, &eqopr, NULL,
+								 NULL);
+		basetypoid = get_base_element_type(stats->attrtypid);
+		if (basetypoid == InvalidOid)
+			basetypoid = stats->attrtypid;
+		get_sort_group_operators(basetypoid,
+								 false, false, false,
+								 &baseltopr, &baseeqopr, NULL,
+								 NULL);
+
+		heap_deform_tuple(tuptable->vals[e], tuptable->tupdesc,
+						  rs_datums, rs_nulls);
+
+		/* These values are not derived from either vac stats or exported stats */
+		values[Anum_pg_statistic_starelid - 1] = ObjectIdGetDatum(InvalidOid);
+		values[Anum_pg_statistic_staattnum - 1] = Int16GetDatum(InvalidAttrNumber);
+		values[Anum_pg_statistic_stainherit - 1] = BoolGetDatum(false);
+
+		if (rs_nulls[EXPR_STANULLFRAC])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Imported stxdepr row cannot have NULL %s", "stanullfrac")));
+
+		if (rs_nulls[EXPR_STAWIDTH])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Imported stxdepr row cannot have NULL %s", "stawidth")));
+
+		if (rs_nulls[EXPR_STADISTINCT])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Imported stxdepr row cannot have NULL %s", "stadistinct")));
+
+		values[Anum_pg_statistic_stanullfrac - 1] = rs_datums[EXPR_STANULLFRAC];
+		values[Anum_pg_statistic_stawidth - 1] = rs_datums[EXPR_STAWIDTH];
+		values[Anum_pg_statistic_stadistinct - 1] = rs_datums[EXPR_STADISTINCT];
+
+		for (k = 0; k < STATISTIC_NUM_SLOTS; k++)
+		{
+			int16	kind = 0;
+			Oid		op = InvalidOid;
+
+			if (!rs_nulls[EXPR_STAKIND1 + k])
+			{
+				kind = Int16GetDatum(rs_datums[EXPR_STAKIND1 + k]);
+
+				if (!rs_nulls[EXPR_EOPRNAME1 + k])
+				{
+					char *s = TextDatumGetCString(rs_datums[EXPR_EOPRNAME1 + k]);
+
+					if (strcmp(s, "=") == 0)
+					{
+						/*
+						 * MCELEM stat arrays are of the same type as the
+						 * array base element type and are eqopr
+						 */
+						if ((kind == STATISTIC_KIND_MCELEM) ||
+							(kind == STATISTIC_KIND_DECHIST))
+							op = baseeqopr;
+						else
+							op = eqopr;
+					}
+					else if (strcmp(s, "<") == 0)
+						op = ltopr;
+					else
+						op = InvalidOid;
+
+					pfree(s);
+				}
+			}
+
+			values[Anum_pg_statistic_stakind1 - 1 + k] = kind;
+			values[Anum_pg_statistic_staop1 - 1 + k] = op;
+
+			/* rely on vacattrstat */
+			values[Anum_pg_statistic_stacoll1 - 1 + k] =
+				ObjectIdGetDatum(stats->stacoll[k]);
+
+			values[Anum_pg_statistic_stanumbers1 - 1 + k] =
+				rs_datums[EXPR_STANUMBERS1 + k];
+			nulls[Anum_pg_statistic_stanumbers1 - 1 + k] =
+				rs_nulls[EXPR_STANUMBERS1 + k];
+
+			nulls[Anum_pg_statistic_stavalues1 - 1 + k] =
+				rs_nulls[EXPR_STAVALUES1 + k];
+			if (rs_nulls[EXPR_STAVALUES1 + k])
+				values[Anum_pg_statistic_stavalues1 - 1 + k] = (Datum) 0;
+			else
+			{
+				char *s = TextDatumGetCString(rs_datums[EXPR_STAVALUES1 + k]);
+
+				values[Anum_pg_statistic_stavalues1 - 1 + k] =
+					FunctionCall3(&finfo, CStringGetDatum(s),
+								  ObjectIdGetDatum(basetypoid),
+								  Int32GetDatum(stats->attrtypmod));
+
+				pfree(s);
+			}
+		}
+
+		pgstup = heap_form_tuple(RelationGetDescr(pgsd), values, nulls);
+
+		astate = accumArrayResult(astate,
+								  heap_copy_tuple_as_datum(pgstup, RelationGetDescr(pgsd)),
+								  false,
+								  pgstypoid,
+								  CurrentMemoryContext);
+	}
+
+	table_close(pgsd, RowExclusiveLock);
+
+	return makeArrayResult(astate, CurrentMemoryContext);
+}
+
+/*
+ * Import statistics for a given extended statistics object.
+ *
+ * The statistics json format is:
+ *
+ * {
+ *   "server_version_num": number, -- SHOW server_version on source system
+ *   "stxoid": number, -- pg_stat_ext.stxoid
+ *   "stxname": string, -- pg_stat_ext.stxname
+ *   "stxnspname": string, -- schema name for the statistics object
+ *   "relname": string, -- pgclass.relname of the exported relation
+ *   "nspname": string, -- schema name for the exported relation
+ *   -- stxkeys cast to text to aid array_in()
+ *   "stxkeys": string, -- pg_statistic_ext.stxkind::text
+ *   -- stxndistinct and stxdndepencies only on v10-v11
+ *   "stxndistinct": string, -- pg_statistic_ext.stxndistinct::text
+ *   "stxdependencies": string, -- pg_statistic_ext.stxdependencies::text
+ *   -- data is on v12+
+ *   "data": [
+ *     {
+ *       -- stxdinherit is on v15+
+ *       "stxdinherit": bool, -- pg_statistic_ext_data.stxdinherit
+ *       -- stxdndistinct and stxddependencies are on v12+
+ *       "stxdndistinct": text, -- pg_statistic_ext_data.stxdndisinct::text
+ *       "stxddependencies": text, -- pg_statistic_ext_data.stxddepencies::text
+ *       -- stxdexpr is on v12+
+ *       "stxdmcv": [
+ *         {
+ *           "index": number,
+ *           "nulls": [bool],
+ *           "values": [text],
+ *           "frequency": number,
+ *           "base_frequency": number
+ *         }
+ *       ],
+ *       -- stxdexpr is on v14+
+ *       "stxdexpr": [
+ *         {
+ *            "staattnum": number, -- pg_statistic.staattnum
+ *            "stainherit": bool, -- pg_statistic.stainherit
+ *            "stanullfrac": number, -- pg_statistic.stanullfrac
+ *            "stawidth": number, -- pg_statistic.stawidth
+ *            "stadistinct": number, -- pg_statistic.stadistinct
+ *            "stakind1": number, -- pg_statistic.stakind1
+ *            "stakind2": number, -- pg_statistic.stakind2
+ *            "stakind3": number, -- pg_statistic.stakind3
+ *            "stakind4": number, -- pg_statistic.stakind4
+ *            "stakind5": number, -- pg_statistic.stakind5
+ *            "staop1": number, -- pg_statistic.staop1
+ *            "staop2": number, -- pg_statistic.staop2
+ *            "staop3": number, -- pg_statistic.staop3
+ *            "staop4": number, -- pg_statistic.staop4
+ *            "staop5": number, -- pg_statistic.staop5
+ *            "stacoll1": number, -- pg_statistic.stacoll1
+ *            "stacoll2": number, -- pg_statistic.stacoll2
+ *            "stacoll3": number, -- pg_statistic.stacoll3
+ *            "stacoll4": number, -- pg_statistic.stacoll4
+ *            "stacoll5": number, -- pg_statistic.stacoll5
+ *            -- stanumbersN are cast to string to aid array_in()
+ *            "stanumbers1": string, -- pg_statistic.stanumbers1::text
+ *            "stanumbers2": string, -- pg_statistic.stanumbers2::text
+ *            "stanumbers3": string, -- pg_statistic.stanumbers3::text
+ *            "stanumbers4": string, -- pg_statistic.stanumbers4::text
+ *            "stanumbers5": string, -- pg_statistic.stanumbers5::text
+ *            -- stavaluesN are cast to string to aid array_in()
+ *            "stavalues1": string, -- pg_statistic.stavalues1::text
+ *            "stavalues2": string, -- pg_statistic.stavalues2::text
+ *            "stavalues3": string, -- pg_statistic.stavalues3::text
+ *            "stavalues4": string, -- pg_statistic.stavalues4::text
+ *            "stavalues5": string -- pg_statistic.stavalues5::text
+ *         }
+ *       ]
+ *     }
+ *   ],
+ *   "types": [
+ *     -- export of all pg_type referenced in this json doc
+ *	   {
+ *        "oid": number, -- pg_type.oid
+ *        "typname": string, -- pg_type.typname
+ *        "nspname": string -- schema name for the pg_type
+ *     }
+ *   ],
+ *   "collations": [
+ *     -- export all pg_collation reference in this json doc
+ *     {
+ *        "oid": number, -- pg_collation.oid
+ *        "collname": string, -- pg_collation.collname
+ *        "nspname": string -- schema name for the pg_collation
+ *     }
+ *   ],
+ *   "operators": [
+ *     -- export all pg_operator reference in this json doc
+ *     {
+ *        "oid": number, -- pg_operator.oid
+ *        "collname": string, -- pg_oprname
+ *        "nspname": string -- schema name for the pg_operator
+ *     }
+ *   ],
+ *   "attributes": [
+ *     -- export all pg_attribute for the exported relation
+ *     {
+ *        "attnum": number, -- pg_attribute.attnum
+ *        "attname": string, -- pg_attribute.attname
+ *        "atttypid": number, -- pg_attribute.atttypid
+ *        "attcollation": number -- pg_attribute.attcollation
+ *     }
+ *   ]
+ *  }
+ *
+ * Each server verion exports a subset of this format. The exported format
+ * can and will change with each new version, and this function will have
+ * to account for those variations.
+ *
+ * Statistics imported from version 15 and higher can potentially have two
+ * result rows, one with stxdinherit = false and one for stxdinherit = true
+ *
+ */
+Datum
+pg_import_ext_stats(PG_FUNCTION_ARGS)
+{
+	const char *bq_sql =
+		"SELECT current_setting('server_version_num') AS current_version, eb.* "
+		"FROM jsonb_to_record($1) AS eb( "
+		"    server_version_num integer, "
+		"    stxoid Oid, "
+		"    reloid Oid, "
+		"    stxname text, "
+		"    stxnspname text, "
+		"    relname text, "
+		"    nspname text, "
+		"    stxkeys text, "
+		"    stxkind text, "
+		"    stxndistinct text, "
+		"    stxdependencies text, "
+		"    data jsonb, "
+		"    attributes jsonb, "
+		"    collations jsonb, "
+		"    operators jsonb, "
+		"    types jsonb) ";
+
+	enum {
+		BQ_CURRENT_VERSION_NUM = 0,
+		BQ_SERVER_VERSION_NUM,
+		BQ_STXOID,
+		BQ_RELOID,
+		BQ_STXNAME,
+		BQ_STXNSPNAME,
+		BQ_RELNAME,
+		BQ_NSPNAME,
+		BQ_STXKEYS,
+		BQ_STXKIND,
+		BQ_STXNDISTINCT,
+		BQ_STXDEPENDENCIES,
+		BQ_DATA,
+		BQ_ATTRIBUTES,
+		BQ_COLLATIONS,
+		BQ_OPERATORS,
+		BQ_TYPES,
+		NUM_BQ_COLS
+	};
+
+	/* All versions of the STXD query have the same column signature */
+	enum {
+		STXD_INHERIT = 0,
+		STXD_NDISTINCT,
+		STXD_DEPENDENCIES,
+		STXD_MCV,
+		STXD_EXPR,
+		NUM_STXD_COLS
+	};
+
+#define BQ_NARGS 1
+
+	Oid		stxid;
+	bool	validate;
+	bool	require_match_oids;
+
+	Oid		bq_argtypes[BQ_NARGS] = { JSONBOID };
+	Datum	bq_args[BQ_NARGS];
+
+	int		ret;
+
+	SPITupleTable  *tuptable;
+
+	Relation	rel;
+	TupleDesc	tupdesc;
+	int			natts;
+
+	HeapTuple	etup;
+	Relation	sd;
+
+	Form_pg_statistic_ext	stxform;
+
+	StatExtEntry   *stxentry;
+	VacAttrStats  **relstats; /* all relations attributes */
+	VacAttrStats  **extstats; /* entries relevenat to the extstat */
+	VacAttrStats  **expr_stats; /* expressions in the extstat */
+	int				nexprs;
+	int				ncols;
+
+	Datum	bq_datums[NUM_BQ_COLS];
+	bool	bq_nulls[NUM_BQ_COLS];
+
+	int		i;
+	int32	server_version_num;
+	int32	current_version_num;
+
+	if (PG_ARGISNULL(0))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("extended statistics oid cannot be NULL")));
+	stxid = PG_GETARG_OID(0);
+
+	if (PG_ARGISNULL(1))
+		PG_RETURN_BOOL(false);
+	bq_args[0] = PG_GETARG_DATUM(1);
+
+	if (PG_ARGISNULL(2))
+		validate = false;
+	else
+		validate = PG_GETARG_BOOL(2);
+
+	if (PG_ARGISNULL(3))
+		require_match_oids = false;
+	else
+		require_match_oids = PG_GETARG_BOOL(3);
+
+	sd = table_open(StatisticRelationId, RowExclusiveLock);
+	etup = SearchSysCacheCopy1(STATEXTOID, ObjectIdGetDatum(stxid));
+	if (!HeapTupleIsValid(etup))
+		elog(ERROR, "pg_statistic_ext entry for oid %u vanished during statistics import",
+			 stxid);
+
+	stxform = (Form_pg_statistic_ext) GETSTRUCT(etup);
+
+	rel = relation_open(stxform->stxrelid, ShareUpdateExclusiveLock);
+
+	tupdesc = RelationGetDescr(rel);
+	natts = tupdesc->natts;
+
+	relstats = (VacAttrStats **) palloc(natts * sizeof(VacAttrStats *));
+	for (i = 0; i < natts; i++)
+		relstats[i] = examine_rel_attribute(rel, i+1, NULL);
+
+	stxentry = create_stat_ext_entry(etup);
+	extstats = lookup_var_attr_stats(rel, stxentry->columns, stxentry->exprs,
+									 natts, relstats);
+
+	/* only the stats that were derived from pg_statistic_ext */
+	ncols = bms_num_members(stxentry->columns);
+	expr_stats = &extstats[ncols];
+	nexprs = list_length(stxentry->exprs);
+
+	/*
+	 * Connect to SPI manager
+	 */
+	if ((ret = SPI_connect()) < 0)
+		elog(ERROR, "SPI connect failure - returned %d", ret);
+
+	/*
+	 * Fetch the base level of the stats json. The results found there will
+	 * determine how the nested data will be handled.
+	 */
+	ret = SPI_execute_with_args(bq_sql, BQ_NARGS, bq_argtypes, bq_args,
+								NULL, true, 1);
+
+	/*
+	 * Only allow one qualifying tuple
+	 */
+	if (ret != SPI_OK_SELECT)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("statistic export JSON is not in proper format")));
+
+	if (SPI_processed != 1)
+		ereport(ERROR,
+				(errcode(ERRCODE_CARDINALITY_VIOLATION),
+				 errmsg("pg_statistic_ext export JSON should return exactly one base object")));
+
+	tuptable = SPI_tuptable;
+	heap_deform_tuple(tuptable->vals[0], tuptable->tupdesc, bq_datums, bq_nulls);
+
+	/*
+	 * Check for valid combination of exported server_version_num to the local
+	 * server_version_num. We won't be reusing these values in a query so use
+	 * scratch datum/null vars.
+	 */
+	if (bq_nulls[BQ_CURRENT_VERSION_NUM])
+		ereport(ERROR,
+				(errcode(ERRCODE_SQL_JSON_ITEM_CANNOT_BE_CAST_TO_TARGET_TYPE),
+				 errmsg("current_version_num cannot be null")));
+
+	if (bq_nulls[BQ_SERVER_VERSION_NUM])
+		ereport(ERROR,
+				(errcode(ERRCODE_SQL_JSON_ITEM_CANNOT_BE_CAST_TO_TARGET_TYPE),
+				 errmsg("server_version_num cannot be null")));
+
+	current_version_num = DatumGetInt32(bq_datums[BQ_CURRENT_VERSION_NUM]);
+	server_version_num = DatumGetInt32(bq_datums[BQ_SERVER_VERSION_NUM]);
+
+	if (server_version_num <= 100000)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("Cannot import statistics from servers below version 10.0")));
+
+	if (server_version_num > current_version_num)
+		ereport(ERROR,
+				(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
+				 errmsg("Cannot import statistics from server version %d to %d",
+						server_version_num, current_version_num)));
+
+	if (validate)
+	{
+		validate_exported_types(bq_datums[BQ_TYPES], bq_nulls[BQ_TYPES]);
+		validate_exported_collations(bq_datums[BQ_COLLATIONS], bq_nulls[BQ_COLLATIONS]);
+		validate_exported_operators(bq_datums[BQ_OPERATORS], bq_nulls[BQ_OPERATORS]);
+		validate_exported_attributes(bq_datums[BQ_ATTRIBUTES], bq_nulls[BQ_ATTRIBUTES]);
+	}
+
+	if (server_version_num >= 120000)
+	{
+		/* pg_statistic_ext_data export for modern versions */
+
+#define STXD_NARGS 1
+
+		Oid		stxd_argtypes[STXD_NARGS] = { JSONBOID };
+		Datum	stxd_args[STXD_NARGS] = { bq_datums[BQ_DATA] };
+		char	stxd_nulls[STXD_NARGS] = { bq_nulls[BQ_DATA] ? 'n' : ' ' };
+
+		const char *stxd_sql =
+			"SELECT d.* "
+			"FROM jsonb_to_recordset($1) AS d ( "
+			"    stxdinherit bool, "
+			"    stxdndistinct text, "
+			"    stxddependencies text, "
+			"    stxdmcv jsonb, "
+			"    stxdexpr jsonb) "
+			"ORDER BY d.stxdinherit ";
+
+		/* Versions 12+ cannot have ndistinct or dependencies on the base query */
+		if (!bq_nulls[BQ_STXNDISTINCT])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Key stxndistinct not allowed on exports of servers v12 and later"),
+					 errhint("Use stxdndistinct instead")));
+
+		if(!bq_nulls[BQ_STXDEPENDENCIES])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Key stxdependencies not allowed on exports of servers v12 and later"),
+					 errhint("Use stxddependencies instead")));
+
+		ret = SPI_execute_with_args(stxd_sql, STXD_NARGS, stxd_argtypes, stxd_args,
+									stxd_nulls, true, 0);
+#undef STXD_NARGS
+	}
+	else
+	{
+#define STXD_NARGS 2
+		Oid		stxd_argtypes[STXD_NARGS] = {
+					TEXTOID,
+					TEXTOID };
+		Datum	stxd_args[STXD_NARGS] = {
+					bq_datums[BQ_STXNDISTINCT],
+					bq_datums[BQ_STXDEPENDENCIES] };
+		char	stxd_nulls[STXD_NARGS] = {
+					bq_nulls[BQ_STXNDISTINCT] ? 'n' : ' ',
+					bq_nulls[BQ_DATA]  ? 'n' : ' ' };
+
+		/* pg_statistic_ext_data export for versions prior to the table existing */
+		const char *stxd_sql =
+			"SELECT "
+			"	NULL::boolean AS stxdinherit, "
+			"   $1 AS stxdndistinct, "
+			"   $2 AS stxddependencies, "
+			"   NULL::jsonb AS stxdmcv, "
+			"   NULL::jsonb AS stxdexpr ";
+
+		ret = SPI_execute_with_args(stxd_sql, STXD_NARGS, stxd_argtypes, stxd_args,
+									stxd_nulls, true, 2);
+
+#undef STXD_NARGS
+	}
+
+	if (ret != SPI_OK_SELECT)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("statistic export JSON is not in proper format")));
+
+	/* overwrite previous tuptable */
+	tuptable = SPI_tuptable;
+
+	for (i = 0; i < tuptable->numvals; i++)
+	{
+		Datum			stxd_datums[NUM_BQ_COLS];
+		bool			stxd_nulls[NUM_BQ_COLS];
+		bool			inh;
+		MCVList		   *mcvlist;
+		MVDependencies *dependencies;
+		MVNDistinct	   *ndistinct;
+		Datum			exprs;
+
+		heap_deform_tuple(tuptable->vals[i], tuptable->tupdesc, stxd_datums,
+						  stxd_nulls);
+
+		if ((!stxd_nulls[STXD_MCV]) && (server_version_num < 120000))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Key stxdmv not allowed on exports of servers berfore v12")));
+
+		if ((!stxd_nulls[STXD_EXPR]) && (server_version_num < 140000))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Key stxdexpr not allowed on exports of servers berfore v14")));
+
+		if ((!stxd_nulls[STXD_INHERIT]) && (server_version_num < 150000))
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Extended statistics from servers prior to v15 cannot contain inherited stats")));
+
+		/* Versions prior to v15 never have stxdinhert set */
+		if (stxd_nulls[STXD_INHERIT])
+			inh = false;
+		else
+			inh = DatumGetBool(stxd_datums[STXD_INHERIT]);
+
+		ndistinct = statext_ndistinct_import(stxform->stxrelid,
+						stxd_datums[STXD_NDISTINCT], stxd_nulls[STXD_NDISTINCT],
+						bq_datums[BQ_ATTRIBUTES], bq_nulls[BQ_ATTRIBUTES]);
+
+		dependencies = statext_dependencies_import(stxform->stxrelid,
+						stxd_datums[STXD_DEPENDENCIES],
+						stxd_nulls[STXD_DEPENDENCIES],
+						bq_datums[BQ_ATTRIBUTES], bq_nulls[BQ_ATTRIBUTES]);
+
+		mcvlist = statext_mcv_import(stxd_datums[STXD_MCV], stxd_nulls[STXD_MCV],
+									 extstats);
+
+		exprs = import_expressions(stxd_datums[STXD_EXPR], stxd_nulls[STXD_EXPR],
+								   bq_datums[BQ_OPERATORS], bq_nulls[BQ_OPERATORS],
+								   expr_stats, nexprs);
+
+		statext_store(stxentry->statOid, inh, ndistinct, dependencies, mcvlist, exprs, extstats);
+	}
+
+	relation_close(rel, NoLock);
+	table_close(sd, RowExclusiveLock);
+	SPI_finish();
+
+	PG_RETURN_BOOL(true);
+}
diff --git a/src/backend/statistics/mcv.c b/src/backend/statistics/mcv.c
index 6255cd1f4f..3bafde83d6 100644
--- a/src/backend/statistics/mcv.c
+++ b/src/backend/statistics/mcv.c
@@ -20,6 +20,7 @@
 #include "catalog/pg_collation.h"
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_statistic_ext_data.h"
+#include "executor/spi.h"
 #include "fmgr.h"
 #include "funcapi.h"
 #include "nodes/nodeFuncs.h"
@@ -2177,3 +2178,194 @@ mcv_clause_selectivity_or(PlannerInfo *root, StatisticExtInfo *stat,
 
 	return s;
 }
+
+/*
+ * statext_mcv_import
+ *
+ * The mcv serialization is the json equivalent of the
+ * pg_mcv_list_items() result set:
+ * [
+ *   {
+ *     "index": number,
+ *     "values": [string],
+ *     "nulls": [bool],
+ *     "frequency": number,
+ *     "base_frequency": number
+ *   }
+ * ]
+ *
+ * The values are text strings that must be converted into datums of the type
+ * appropriate for their corresponding dimension. This means that we must
+ * cast individual datums rather than trying to use array_in().
+ *
+ */
+MCVList *
+statext_mcv_import(Datum mcv, bool mcv_null, VacAttrStats **extstats)
+{
+	const char *sql =
+		"SELECT m.*, array_length(m.nulls,1) AS ndims "
+		"FROM jsonb_to_recordset($1) AS m(index integer, values text[], "
+		"         nulls boolean[], frequency float8, base_frequency float8) "
+		"ORDER BY m.index ";
+
+	enum {
+		MCVS_INDEX = 0,
+		MCVS_VALUES,
+		MCVS_NULLS,
+		MCVS_FREQUENCY,
+		MCVS_BASE_FREQUENCY,
+		MCVS_NDIMS,
+		NUM_MCVS_COLS
+	};
+
+#define MCVS_NARGS 1
+
+	Oid		argtypes[MCVS_NARGS] = { JSONBOID };
+	Datum	args[MCVS_NARGS] = { mcv };
+	char	argnulls[MCVS_NARGS] = { mcv_null ? 'n' : ' ' };
+	int		nitems = 0;
+	int		ndims = 0;
+	int		ret;
+	int		i;
+
+	MCVList		   *mcvlist;
+	SPITupleTable  *tuptable;
+	Oid				ioparams[STATS_MAX_DIMENSIONS];
+	FmgrInfo		finfos[STATS_MAX_DIMENSIONS];
+
+	ret = SPI_execute_with_args(sql, MCVS_NARGS, argtypes, args, argnulls, true, 0);
+
+	if (ret != SPI_OK_SELECT)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("statistic export JSON is not in proper format")));
+
+	tuptable = SPI_tuptable;
+	if (tuptable->numvals > 0)
+	{
+		/* ndims will be same for all rows, so just check first one */
+		bool	isnull;
+		Datum	d = SPI_getbinval(tuptable->vals[0], tuptable->tupdesc,
+								  MCVS_NDIMS+1, &isnull);
+
+		if (isnull)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Indeterminate number of mcv dimensions")));
+
+		ndims = DatumGetInt32(d);
+		nitems = tuptable->numvals;
+	}
+
+	mcvlist = (MCVList *) palloc0(offsetof(MCVList, items) +
+								  (sizeof(MCVItem) * nitems));
+
+	mcvlist->magic = STATS_MCV_MAGIC;
+	mcvlist->type = STATS_MCV_TYPE_BASIC;
+	mcvlist->nitems = nitems;
+	mcvlist->ndimensions = ndims;
+
+	/* We will need these input functions $nitems times. */
+	for (i = 0; i < ndims; i++)
+	{
+		Oid		typid = extstats[i]->attrtypid;
+		Oid		infunc;
+
+		mcvlist->types[i] = typid;
+		getTypeInputInfo(typid, &infunc, &ioparams[i]);
+		fmgr_info(infunc, &finfos[i]);
+	}
+
+	for (i = 0; i < nitems; i++)
+	{
+		MCVItem	   *item = &mcvlist->items[i];
+		Datum		datums[NUM_MCVS_COLS];
+		bool		nulls[NUM_MCVS_COLS];
+		ArrayType  *arr;
+		Datum      *elems;
+		bool       *elnulls;
+		int         nelems;
+
+		int			d;
+
+		heap_deform_tuple(tuptable->vals[i], tuptable->tupdesc, datums, nulls);
+
+		if (nulls[MCVS_VALUES])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("extended statistic mcv export cannot have NULL %s",
+							"values")));
+		if (nulls[MCVS_NULLS])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("extended statistic mcv export cannot have NULL %s",
+							"nulls")));
+		if (nulls[MCVS_FREQUENCY])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("extended statistic mcv export cannot have NULL %s",
+							"frequency")));
+		if (nulls[MCVS_BASE_FREQUENCY])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("extended statistic mcv export cannot have NULL %s",
+							"base_frequency")));
+
+		item->frequency = DatumGetFloat8(datums[MCVS_FREQUENCY]);
+		item->base_frequency = DatumGetFloat8(datums[MCVS_BASE_FREQUENCY]);
+		item->values = (Datum *) palloc(sizeof(Datum) * ndims);
+		item->isnull = (bool *) palloc(sizeof(bool) * ndims);
+
+		arr = DatumGetArrayTypeP(datums[MCVS_NULLS]);
+		deconstruct_array(arr, BOOLOID, 1, true, 'c', &elems, &elnulls, &nelems);
+
+		if (nelems != ndims)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("extended statistic mcv %s array expected %d elements but %d found",
+							"nulls", ndims, nelems)));
+
+		for (d = 0; d < ndims; d++)
+		{
+			if (elnulls[d])
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("extended statistic mcv %s array cannot contain NULL values",
+								"nulls")));
+			item->isnull[d] = DatumGetBool(elems[d]);
+		}
+
+		arr = DatumGetArrayTypeP(datums[MCVS_VALUES]);
+		deconstruct_array_builtin(arr, TEXTOID, &elems, &elnulls, &nelems);
+
+		if (nelems != ndims)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("extended statistic mcv %s array expected %d elements but %d found",
+							"values", ndims, nelems)));
+
+		for (d = 0; d < ndims; d++)
+		{
+			/* if the element is a known NULL, nothing to decode */
+			if (item->isnull[d])
+				item->values[d] = (Datum) 0;
+			else
+			{
+				char   *s;
+
+				if (elnulls[d])
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("extended statistic mcv nulls array in conflict with values array")));
+
+				s = TextDatumGetCString(elems[d]);
+
+				item->values[d] = InputFunctionCall(&finfos[d], s, ioparams[d],
+													extstats[d]->attrtypmod);
+				pfree(s);
+			}
+		}
+	}
+
+	return mcvlist;
+}
diff --git a/src/backend/statistics/mvdistinct.c b/src/backend/statistics/mvdistinct.c
index ee1134cc37..d84eee47ee 100644
--- a/src/backend/statistics/mvdistinct.c
+++ b/src/backend/statistics/mvdistinct.c
@@ -28,9 +28,11 @@
 #include "access/htup_details.h"
 #include "catalog/pg_statistic_ext.h"
 #include "catalog/pg_statistic_ext_data.h"
+#include "executor/spi.h"
 #include "lib/stringinfo.h"
 #include "statistics/extended_stats_internal.h"
 #include "statistics/statistics.h"
+#include "utils/builtins.h"
 #include "utils/fmgrprotos.h"
 #include "utils/lsyscache.h"
 #include "utils/syscache.h"
@@ -698,3 +700,161 @@ generate_combinations(CombinationGenerator *state)
 
 	pfree(current);
 }
+
+/*
+ * statext_dependencies_import
+ *
+ * The ndinstinct serialization is a string that looks like
+ *       {"2, 3": 1521, "3, -1": 4}
+ *
+ *   This structure can be coerced into JSON, but we must use JSON
+ *   over JSONB because JSON preserves key order and JSONB does not.
+ *
+ *   The key side integers represent attnums in the exported table, and these
+ *   may not line up with the attnums in the destination table so we match
+ *   them by name.
+ *
+ *   Negative integers represent expressions columns that have no
+ *   corresponding match in the exported attributes. We leave those
+ *   attnums as-is. Positive integers are looked up in the exported
+ *   attributes and the attname there is then compared to pg_attribute
+ *   names in the underlying table, and that tuples attnum is used instead.
+ */
+MVNDistinct *
+statext_ndistinct_import(Oid relid, Datum ndistinct, bool ndistinct_null,
+						 Datum attributes, bool attributes_null)
+{
+	MVNDistinct	   *result;
+	int				nitems;
+
+#define NDIST_NARGS 3
+
+	Oid			argtypes[NDIST_NARGS] = { OIDOID, TEXTOID, JSONBOID };
+	Datum		args[NDIST_NARGS] = { relid, ndistinct , attributes };
+	char		argnulls[NDIST_NARGS] = { ' ',
+					ndistinct_null ? 'n' : ' ',
+					attributes_null ? 'n' : ' ' };
+
+	const char *sql =
+		"SELECT "
+		"    i.itemord, "
+		"    a.attrord, "
+		"    a.exp_attnum, "
+		"    ea.attname AS exp_attname, "
+		"    CASE "
+		"        WHEN a.exp_attnum < 0 THEN a.exp_attnum "
+		"        ELSE pga.attnum "
+		"    END AS attnum, "
+		"    i.ndistinct::float8 AS ndistinct, "
+		"    COUNT(*) OVER (PARTITION BY i.itemord) AS num_attrs, "
+		"    MAX(i.itemord) OVER () AS num_items "
+		"FROM json_each_text($2::json) "
+		"     WITH ORDINALITY AS i(attrlist, ndistinct, itemord) "
+		"CROSS JOIN LATERAL unnest(string_to_array(i.attrlist, ', ')::int2[]) "
+		"     WITH ORDINALITY AS a(exp_attnum, attrord) "
+		"LEFT JOIN LATERAL jsonb_to_recordset($3) AS ea(attnum int2, attname text) "
+		"     ON ea.attnum = a.exp_attnum AND a.exp_attnum > 0 "
+		"LEFT JOIN pg_attribute AS pga "
+		"     ON pga.attrelid = $1 AND pga.attname = ea.attname "
+		"ORDER BY i.itemord, a.attrord ";
+
+	enum {
+		NDIST_ITEMORD = 0,
+		NDIST_ATTRORD,
+		NDIST_EXP_ATTNUM,
+		NDIST_EXP_ATTNAME,
+		NDIST_ATTNUM,
+		NDIST_NDISTINCT,
+		NDIST_NUM_ATTRS,
+		NDIST_NUM_ITEMS,
+		NUM_NDIST_COLS
+	};
+
+	SPITupleTable  *tuptable;
+	int				ret;
+	int				j;
+
+	ret = SPI_execute_with_args(sql, NDIST_NARGS, argtypes, args, argnulls, true, 0);
+
+	if (ret != SPI_OK_SELECT)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("statistic export JSON is not in proper format")));
+
+	tuptable = SPI_tuptable;
+	if (tuptable->numvals == 0)
+		nitems = 0;
+	else
+	{
+		bool isnull;
+		Datum d = SPI_getbinval(tuptable->vals[0], tuptable->tupdesc,
+								NDIST_NUM_ITEMS+1, &isnull);
+		nitems = DatumGetInt32(d);
+
+		if (isnull)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Indeterminate number of dependencies")));
+	}
+
+	result = palloc(offsetof(MVNDistinct, items) +
+					(nitems * sizeof(MVNDistinctItem)));
+	result->magic = STATS_NDISTINCT_MAGIC;
+	result->type = STATS_NDISTINCT_TYPE_BASIC;
+	result->nitems = nitems;
+
+	for (j = 0; j < tuptable->numvals; j++)
+	{
+		Datum	datums[NUM_NDIST_COLS];
+		bool	nulls[NUM_NDIST_COLS];
+		int		i;
+		int		a;
+		int		natts;
+
+		MVNDistinctItem *item;
+
+		heap_deform_tuple(tuptable->vals[j], tuptable->tupdesc, datums, nulls);
+
+		Assert(!nulls[NDIST_ITEMORD]);
+		i = DatumGetInt32(datums[NDIST_ITEMORD]) - 1;
+		item = &result->items[i];
+		Assert(!nulls[NDIST_ATTRORD]);
+		a = DatumGetInt32(datums[NDIST_ATTRORD]) - 1;
+
+		if (a == 0)
+		{
+			/* New item */
+			Assert(!nulls[NDIST_NUM_ATTRS]);
+			natts = DatumGetInt32(datums[NDIST_NUM_ATTRS]);
+			item->nattributes = natts;
+			item->attributes = palloc(sizeof(AttrNumber) * natts);
+			Assert(!nulls[NDIST_NDISTINCT]);
+			item->ndistinct = DatumGetFloat8(datums[NDIST_NDISTINCT]);
+		}
+
+		if (!nulls[NDIST_ATTNUM])
+			item->attributes[a] =
+				DatumGetInt16(datums[NDIST_ATTNUM]);
+		else if (nulls[NDIST_EXP_ATTNUM])
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("ndistinct exported attnum cannot be null")));
+		else
+		{
+			AttrNumber exp_attnum = DatumGetInt16(datums[NDIST_EXP_ATTNUM]);
+
+			if (nulls[NDIST_EXP_ATTNAME])
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("ndistinct has no exported name for attnum %d",
+								exp_attnum)));
+
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Dependency tried to match attnum %d by name (%s) but found no match",
+						 exp_attnum, TextDatumGetCString(datums[NDIST_EXP_ATTNAME]))));
+		}
+	}
+
+	return result;
+}
diff --git a/src/test/regress/expected/stats_export_import.out b/src/test/regress/expected/stats_export_import.out
index 5ab51c5aa0..9d17947583 100644
--- a/src/test/regress/expected/stats_export_import.out
+++ b/src/test/regress/expected/stats_export_import.out
@@ -22,6 +22,7 @@ SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_export_import.comple
 UNION ALL
 SELECT 4, 'four', NULL, NULL;
 CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+CREATE STATISTICS stats_export_import.evens_test ON name, ((comp).a % 2 = 0) FROM stats_export_import.test;
 -- Generate statistics on table with data
 ANALYZE stats_export_import.test;
 -- Capture pg_statistic values for table and index
@@ -44,6 +45,25 @@ FROM stats_export_import.pg_statistic_capture;
      5
 (1 row)
 
+-- Capture pg_statistic values for table and index
+CREATE TABLE stats_export_import.pg_statistic_ext_data_capture
+AS
+SELECT  d.stxdinherit,
+        d.stxdndistinct::text AS stxdndistinct,
+        d.stxddependencies::text AS stxddependencies,
+        d.stxdmcv::text AS stxdmcv,
+        d.stxdexpr::text AS stxdexpr
+FROM pg_statistic_ext AS e
+JOIN pg_statistic_ext_data AS d ON d.stxoid = e.oid
+WHERE e.stxrelid = 'stats_export_import.test'::regclass
+AND e.stxname = 'evens_test';
+SELECT COUNT(*)
+FROM stats_export_import.pg_statistic_ext_data_capture;
+ count 
+-------
+     1
+(1 row)
+
 -- Export stats
 SELECT
     jsonb_build_object(
@@ -323,6 +343,173 @@ WHERE :'debug'::boolean;
 ------------------
 (0 rows)
 
+SELECT
+    jsonb_build_object(
+        'server_version_num', current_setting('server_version_num'),
+        'stxoid', e.oid,
+        'reloid', r.oid,
+        'stxname', e.stxname,
+        'stxnspname', en.nspname,
+        'relname', r.relname,
+        'nspname', n.nspname,
+        'stxkeys', e.stxkeys::text,
+        'stxkind', e.stxkind::text,
+        'data',
+        (
+            SELECT
+                array_agg(r ORDER by r.stxdinherit)
+            FROM (
+                SELECT
+                    sd.stxdinherit,
+                    sd.stxdndistinct::text AS stxdndistinct,
+                    sd.stxddependencies::text AS stxddependencies,
+                    (
+                        SELECT
+                            array_agg(mcvl)
+                        FROM pg_mcv_list_items(sd.stxdmcv) AS mcvl
+                        WHERE sd.stxdmcv IS NOT NULL
+                    ) AS stxdmcv,
+                    (
+                        SELECT
+                            array_agg(r ORDER BY r.stainherit, r.staattnum)
+                        FROM (
+                            SELECT
+                                 s.staattnum,
+                                 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::text AS stanumbers1,
+                                 s.stanumbers2::text AS stanumbers2,
+                                 s.stanumbers3::text AS stanumbers3,
+                                 s.stanumbers4::text AS stanumbers4,
+                                 s.stanumbers5::text AS stanumbers5,
+                                 s.stavalues1::text AS stavalues1,
+                                 s.stavalues2::text AS stavalues2,
+                                 s.stavalues3::text AS stavalues3,
+                                 s.stavalues4::text AS stavalues4,
+                                 s.stavalues5::text AS stavalues5
+                            FROM unnest(sd.stxdexpr) AS s
+                            WHERE sd.stxdexpr IS NOT NULL
+                        ) AS r
+                    ) AS stxdexpr
+                FROM pg_statistic_ext_data AS sd
+                WHERE sd.stxoid = e.oid
+            ) r
+        ),
+        'types',
+        (
+            SELECT
+                array_agg(r)
+            FROM (
+                SELECT
+                    a.atttypid AS oid,
+                    t.typname,
+                    n.nspname
+                FROM pg_attribute AS a
+                JOIN pg_type AS t ON t.oid = a.atttypid
+                JOIN pg_namespace AS n ON n.oid = t.typnamespace
+                WHERE a.attrelid = r.oid
+                AND NOT a.attisdropped
+                AND a.attnum > 0
+            ) AS r
+        ),
+        'collations',
+        (
+            SELECT
+                array_agg(r)
+            FROM (
+                SELECT
+                    e.oid,
+                    c.collname,
+                    n.nspname
+                FROM (
+                    SELECT a.attcollation AS oid
+                    FROM pg_attribute AS a
+                    WHERE a.attrelid = r.oid
+                    AND NOT a.attisdropped
+                    AND a.attnum > 0
+                    UNION
+                    SELECT u.collid
+                    FROM pg_statistic_ext_data AS sd
+                    CROSS JOIN LATERAL unnest(sd.stxdexpr) AS s
+                    CROSS JOIN LATERAL unnest(ARRAY[
+                                        s.stacoll1, s.stacoll2,
+                                        s.stacoll3, s.stacoll4,
+                                        s.stacoll5]) AS u(collid)
+                    WHERE sd.stxoid = e.oid
+                    AND sd.stxdexpr IS NOT NULL
+                    ) AS e
+                JOIN pg_collation AS c ON c.oid = e.oid
+                JOIN pg_namespace AS n ON n.oid = c.collnamespace
+            ) AS r
+        ),
+        'operators',
+        (
+            SELECT
+                array_agg(r)
+            FROM (
+                SELECT DISTINCT
+                    o.oid,
+                    o.oprname,
+                    n.nspname
+                FROM pg_statistic_ext_data AS sd
+                CROSS JOIN LATERAL unnest(sd.stxdexpr) AS s
+                CROSS JOIN LATERAL unnest(ARRAY[
+                                    s.staop1, s.staop2,
+                                    s.staop3, s.staop4,
+                                    s.staop5]) AS u(opid)
+                JOIN pg_operator AS o ON o.oid = u.oid
+                JOIN pg_namespace AS n ON n.oid = o.oprnamespace
+                WHERE sd.stxoid = e.oid
+                AND sd.stxdexpr IS NOT NULL
+            ) AS r
+        ),
+        'attributes',
+        (
+            SELECT
+                array_agg(r ORDER BY r.attnum)
+            FROM (
+                SELECT
+                    a.attnum,
+                    a.attname,
+                    a.atttypid,
+                    a.attcollation
+                FROM pg_attribute AS a
+                WHERE a.attrelid = r.oid
+                AND NOT a.attisdropped
+                AND a.attnum > 0
+            ) AS r
+        )
+    ) AS ext_stats_json
+FROM pg_class r
+JOIN pg_statistic_ext AS e ON e.stxrelid = r.oid
+JOIN pg_namespace AS en ON en.oid = e.stxnamespace
+JOIN pg_namespace AS n ON n.oid = r.relnamespace
+WHERE e.stxrelid = 'stats_export_import.test'::regclass
+AND e.stxname = 'evens_test'
+\gset
+SELECT jsonb_pretty(:'ext_stats_json'::jsonb) AS ext_stats_json
+WHERE :'debug'::boolean;
+ ext_stats_json 
+----------------
+(0 rows)
+
 SELECT relname, reltuples
 FROM pg_class
 WHERE oid IN ('stats_export_import.test'::regclass,
@@ -334,12 +521,14 @@ ORDER BY relname;
  test    |         4
 (2 rows)
 
--- Move table and index out of the way
+-- Move table and index and extended stats out of the way
 ALTER TABLE stats_export_import.test RENAME TO test_orig;
 ALTER INDEX stats_export_import.is_odd RENAME TO is_odd_orig;
--- Create empty copy tables
+ALTER STATISTICS stats_export_import.evens_test RENAME TO evens_test_orig;
+-- Create empty copy tables and objects
 CREATE TABLE stats_export_import.test(LIKE stats_export_import.test_orig);
 CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+CREATE STATISTICS stats_export_import.evens_test ON name, ((comp).a % 2 = 0) FROM stats_export_import.test;
 -- Verify no stats for these new tables
 SELECT COUNT(*)
 FROM pg_statistic
@@ -475,6 +664,19 @@ SELECT pg_import_rel_stats(
  t
 (1 row)
 
+SELECT pg_import_ext_stats(
+        e.oid,
+        :'ext_stats_json'::jsonb,
+        true,
+        true)
+FROM pg_statistic_ext AS e
+WHERE e.stxrelid = 'stats_export_import.test'::regclass
+AND e.stxname = 'evens_test';
+ pg_import_ext_stats 
+---------------------
+ t
+(1 row)
+
 -- This should return 0 rows
 SELECT staattnum, stainherit, stanullfrac, stawidth, stadistinct,
         stakind1, stakind2, stakind3, stakind4, stakind5,
@@ -528,3 +730,62 @@ ORDER BY relname;
  test    |         4
 (2 rows)
 
+-- This should return 0 rows
+SELECT  d.stxdinherit,
+        d.stxdndistinct::text AS stxdndistinct,
+        d.stxddependencies::text AS stxddependencies,
+        d.stxdmcv::text AS stxdmcv,
+        d.stxdexpr::text AS stxdexpr
+FROM pg_statistic_ext AS e
+JOIN pg_statistic_ext_data AS d ON d.stxoid = e.oid
+WHERE e.stxrelid = 'stats_export_import.test'::regclass
+AND e.stxname = 'evens_test'
+EXCEPT
+SELECT *
+FROM stats_export_import.pg_statistic_ext_data_capture;
+ stxdinherit | stxdndistinct | stxddependencies | stxdmcv | stxdexpr 
+-------------+---------------+------------------+---------+----------
+(0 rows)
+
+-- This should return 0 rows
+SELECT *
+FROM stats_export_import.pg_statistic_ext_data_capture
+EXCEPT
+SELECT  d.stxdinherit,
+        d.stxdndistinct::text AS stxdndistinct,
+        d.stxddependencies::text AS stxddependencies,
+        d.stxdmcv::text AS stxdmcv,
+        d.stxdexpr::text AS stxdexpr
+FROM pg_statistic_ext AS e
+JOIN pg_statistic_ext_data AS d ON d.stxoid = e.oid
+WHERE e.stxrelid = 'stats_export_import.test'::regclass
+AND e.stxname = 'evens_test';
+ stxdinherit | stxdndistinct | stxddependencies | stxdmcv | stxdexpr 
+-------------+---------------+------------------+---------+----------
+(0 rows)
+
+SELECT staattnum, 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
+FROM stats_export_import.pg_statistic_capture
+WHERE :'debug'::boolean;
+ staattnum | 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 
+-----------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----
+(0 rows)
+
+SELECT staattnum, stainherit, stanullfrac, stawidth, stadistinct,
+        stakind1, stakind2, stakind3, stakind4, stakind5,
+        staop1, staop2, staop3, staop4, staop5, stacoll1, stacoll2, stacoll3, stacoll4, stacoll5,
+        stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic
+WHERE starelid IN ('stats_export_import.test'::regclass,
+                    'stats_export_import.is_odd'::regclass)
+AND :'debug'::boolean;
+ staattnum | 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 
+-----------+------------+-------------+----------+-------------+----------+----------+----------+----------+----------+--------+--------+--------+--------+--------+----------+----------+----------+----------+----------+-------------+-------------+-------------+-------------+-------------+-----+-----+-----+-----+-----
+(0 rows)
+
diff --git a/src/test/regress/sql/stats_export_import.sql b/src/test/regress/sql/stats_export_import.sql
index 9a80eebeec..cbe94b9273 100644
--- a/src/test/regress/sql/stats_export_import.sql
+++ b/src/test/regress/sql/stats_export_import.sql
@@ -26,6 +26,7 @@ UNION ALL
 SELECT 4, 'four', NULL, NULL;
 
 CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+CREATE STATISTICS stats_export_import.evens_test ON name, ((comp).a % 2 = 0) FROM stats_export_import.test;
 
 -- Generate statistics on table with data
 ANALYZE stats_export_import.test;
@@ -47,6 +48,22 @@ WHERE starelid IN ('stats_export_import.test'::regclass,
 SELECT COUNT(*)
 FROM stats_export_import.pg_statistic_capture;
 
+-- Capture pg_statistic values for table and index
+CREATE TABLE stats_export_import.pg_statistic_ext_data_capture
+AS
+SELECT  d.stxdinherit,
+        d.stxdndistinct::text AS stxdndistinct,
+        d.stxddependencies::text AS stxddependencies,
+        d.stxdmcv::text AS stxdmcv,
+        d.stxdexpr::text AS stxdexpr
+FROM pg_statistic_ext AS e
+JOIN pg_statistic_ext_data AS d ON d.stxoid = e.oid
+WHERE e.stxrelid = 'stats_export_import.test'::regclass
+AND e.stxname = 'evens_test';
+
+SELECT COUNT(*)
+FROM stats_export_import.pg_statistic_ext_data_capture;
+
 -- Export stats
 SELECT
     jsonb_build_object(
@@ -322,19 +339,186 @@ WHERE r.oid = 'stats_export_import.is_odd'::regclass
 SELECT jsonb_pretty(:'index_stats_json'::jsonb) AS index_stats_json
 WHERE :'debug'::boolean;
 
+SELECT
+    jsonb_build_object(
+        'server_version_num', current_setting('server_version_num'),
+        'stxoid', e.oid,
+        'reloid', r.oid,
+        'stxname', e.stxname,
+        'stxnspname', en.nspname,
+        'relname', r.relname,
+        'nspname', n.nspname,
+        'stxkeys', e.stxkeys::text,
+        'stxkind', e.stxkind::text,
+        'data',
+        (
+            SELECT
+                array_agg(r ORDER by r.stxdinherit)
+            FROM (
+                SELECT
+                    sd.stxdinherit,
+                    sd.stxdndistinct::text AS stxdndistinct,
+                    sd.stxddependencies::text AS stxddependencies,
+                    (
+                        SELECT
+                            array_agg(mcvl)
+                        FROM pg_mcv_list_items(sd.stxdmcv) AS mcvl
+                        WHERE sd.stxdmcv IS NOT NULL
+                    ) AS stxdmcv,
+                    (
+                        SELECT
+                            array_agg(r ORDER BY r.stainherit, r.staattnum)
+                        FROM (
+                            SELECT
+                                 s.staattnum,
+                                 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::text AS stanumbers1,
+                                 s.stanumbers2::text AS stanumbers2,
+                                 s.stanumbers3::text AS stanumbers3,
+                                 s.stanumbers4::text AS stanumbers4,
+                                 s.stanumbers5::text AS stanumbers5,
+                                 s.stavalues1::text AS stavalues1,
+                                 s.stavalues2::text AS stavalues2,
+                                 s.stavalues3::text AS stavalues3,
+                                 s.stavalues4::text AS stavalues4,
+                                 s.stavalues5::text AS stavalues5
+                            FROM unnest(sd.stxdexpr) AS s
+                            WHERE sd.stxdexpr IS NOT NULL
+                        ) AS r
+                    ) AS stxdexpr
+                FROM pg_statistic_ext_data AS sd
+                WHERE sd.stxoid = e.oid
+            ) r
+        ),
+        'types',
+        (
+            SELECT
+                array_agg(r)
+            FROM (
+                SELECT
+                    a.atttypid AS oid,
+                    t.typname,
+                    n.nspname
+                FROM pg_attribute AS a
+                JOIN pg_type AS t ON t.oid = a.atttypid
+                JOIN pg_namespace AS n ON n.oid = t.typnamespace
+                WHERE a.attrelid = r.oid
+                AND NOT a.attisdropped
+                AND a.attnum > 0
+            ) AS r
+        ),
+        'collations',
+        (
+            SELECT
+                array_agg(r)
+            FROM (
+                SELECT
+                    e.oid,
+                    c.collname,
+                    n.nspname
+                FROM (
+                    SELECT a.attcollation AS oid
+                    FROM pg_attribute AS a
+                    WHERE a.attrelid = r.oid
+                    AND NOT a.attisdropped
+                    AND a.attnum > 0
+                    UNION
+                    SELECT u.collid
+                    FROM pg_statistic_ext_data AS sd
+                    CROSS JOIN LATERAL unnest(sd.stxdexpr) AS s
+                    CROSS JOIN LATERAL unnest(ARRAY[
+                                        s.stacoll1, s.stacoll2,
+                                        s.stacoll3, s.stacoll4,
+                                        s.stacoll5]) AS u(collid)
+                    WHERE sd.stxoid = e.oid
+                    AND sd.stxdexpr IS NOT NULL
+                    ) AS e
+                JOIN pg_collation AS c ON c.oid = e.oid
+                JOIN pg_namespace AS n ON n.oid = c.collnamespace
+            ) AS r
+        ),
+        'operators',
+        (
+            SELECT
+                array_agg(r)
+            FROM (
+                SELECT DISTINCT
+                    o.oid,
+                    o.oprname,
+                    n.nspname
+                FROM pg_statistic_ext_data AS sd
+                CROSS JOIN LATERAL unnest(sd.stxdexpr) AS s
+                CROSS JOIN LATERAL unnest(ARRAY[
+                                    s.staop1, s.staop2,
+                                    s.staop3, s.staop4,
+                                    s.staop5]) AS u(opid)
+                JOIN pg_operator AS o ON o.oid = u.oid
+                JOIN pg_namespace AS n ON n.oid = o.oprnamespace
+                WHERE sd.stxoid = e.oid
+                AND sd.stxdexpr IS NOT NULL
+            ) AS r
+        ),
+        'attributes',
+        (
+            SELECT
+                array_agg(r ORDER BY r.attnum)
+            FROM (
+                SELECT
+                    a.attnum,
+                    a.attname,
+                    a.atttypid,
+                    a.attcollation
+                FROM pg_attribute AS a
+                WHERE a.attrelid = r.oid
+                AND NOT a.attisdropped
+                AND a.attnum > 0
+            ) AS r
+        )
+    ) AS ext_stats_json
+FROM pg_class r
+JOIN pg_statistic_ext AS e ON e.stxrelid = r.oid
+JOIN pg_namespace AS en ON en.oid = e.stxnamespace
+JOIN pg_namespace AS n ON n.oid = r.relnamespace
+WHERE e.stxrelid = 'stats_export_import.test'::regclass
+AND e.stxname = 'evens_test'
+\gset
+
+SELECT jsonb_pretty(:'ext_stats_json'::jsonb) AS ext_stats_json
+WHERE :'debug'::boolean;
+
 SELECT relname, reltuples
 FROM pg_class
 WHERE oid IN ('stats_export_import.test'::regclass,
               'stats_export_import.is_odd'::regclass)
 ORDER BY relname;
 
--- Move table and index out of the way
+-- Move table and index and extended stats out of the way
 ALTER TABLE stats_export_import.test RENAME TO test_orig;
 ALTER INDEX stats_export_import.is_odd RENAME TO is_odd_orig;
+ALTER STATISTICS stats_export_import.evens_test RENAME TO evens_test_orig;
 
--- Create empty copy tables
+-- Create empty copy tables and objects
 CREATE TABLE stats_export_import.test(LIKE stats_export_import.test_orig);
 CREATE INDEX is_odd ON stats_export_import.test(((comp).a % 2 = 1));
+CREATE STATISTICS stats_export_import.evens_test ON name, ((comp).a % 2 = 0) FROM stats_export_import.test;
 
 -- Verify no stats for these new tables
 SELECT COUNT(*)
@@ -456,6 +640,15 @@ SELECT pg_import_rel_stats(
         true,
         true);
 
+SELECT pg_import_ext_stats(
+        e.oid,
+        :'ext_stats_json'::jsonb,
+        true,
+        true)
+FROM pg_statistic_ext AS e
+WHERE e.stxrelid = 'stats_export_import.test'::regclass
+AND e.stxname = 'evens_test';
+
 -- This should return 0 rows
 SELECT staattnum, stainherit, stanullfrac, stawidth, stadistinct,
         stakind1, stakind2, stakind3, stakind4, stakind5,
@@ -497,3 +690,51 @@ FROM pg_class
 WHERE oid IN ('stats_export_import.test'::regclass,
               'stats_export_import.is_odd'::regclass)
 ORDER BY relname;
+
+-- This should return 0 rows
+SELECT  d.stxdinherit,
+        d.stxdndistinct::text AS stxdndistinct,
+        d.stxddependencies::text AS stxddependencies,
+        d.stxdmcv::text AS stxdmcv,
+        d.stxdexpr::text AS stxdexpr
+FROM pg_statistic_ext AS e
+JOIN pg_statistic_ext_data AS d ON d.stxoid = e.oid
+WHERE e.stxrelid = 'stats_export_import.test'::regclass
+AND e.stxname = 'evens_test'
+EXCEPT
+SELECT *
+FROM stats_export_import.pg_statistic_ext_data_capture;
+
+-- This should return 0 rows
+SELECT *
+FROM stats_export_import.pg_statistic_ext_data_capture
+EXCEPT
+SELECT  d.stxdinherit,
+        d.stxdndistinct::text AS stxdndistinct,
+        d.stxddependencies::text AS stxddependencies,
+        d.stxdmcv::text AS stxdmcv,
+        d.stxdexpr::text AS stxdexpr
+FROM pg_statistic_ext AS e
+JOIN pg_statistic_ext_data AS d ON d.stxoid = e.oid
+WHERE e.stxrelid = 'stats_export_import.test'::regclass
+AND e.stxname = 'evens_test';
+
+
+SELECT staattnum, 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
+FROM stats_export_import.pg_statistic_capture
+WHERE :'debug'::boolean;
+
+SELECT staattnum, stainherit, stanullfrac, stawidth, stadistinct,
+        stakind1, stakind2, stakind3, stakind4, stakind5,
+        staop1, staop2, staop3, staop4, staop5, stacoll1, stacoll2, stacoll3, stacoll4, stacoll5,
+        stanumbers1, stanumbers2, stanumbers3, stanumbers4, stanumbers5,
+        stavalues1::text AS sv1, stavalues2::text AS sv2, stavalues3::text AS sv3,
+        stavalues4::text AS sv4, stavalues5::text AS sv5
+FROM pg_statistic
+WHERE starelid IN ('stats_export_import.test'::regclass,
+                    'stats_export_import.is_odd'::regclass)
+AND :'debug'::boolean;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index d16983dff3..c225d937e9 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -28735,12 +28735,38 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
        </para>
        <para>
         If <parameter>require_match_oids</parameter> is set to <literal>true</literal>,
-        then the import will fail if the imported oids for <structname>pt_type</structname>,
+        then the import will fail if the imported oids for <structname>pg_type</structname>,
         <structname>pg_collation</structname>, and <structname>pg_operator</structname> do
         not match the values specified in <parameter>relation_json</parameter>, as would be expected
         in a binary upgrade. These assumptions would not be true when restoring from a dump.
        </para></entry>
       </row>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_import_ext_stats</primary>
+        </indexterm>
+        <function>pg_import_ext_stats</function> ( <parameter>extended statisticss object</parameter> <type>oid</type>, <parameter>extended_stats</parameter> <type>jsonb</type> <parameter>validate</parameter> <type>boolean</type>, <parameter>require_match_oids</parameter> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Modifies the <structname>pg_statistic_ext_data</structname> rows for the
+        <structfield>oid</structfield> matching
+        <parameter>extended statistics object</parameter> are transactionally
+        replaced with the values found in <parameter>extended_stats</parameter>.
+        The purpose of this function is to apply statistics values in an upgrade
+        situation that are "good enough" for system operation until they are
+        replaced by the next auto-analyze. This function could be 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>
+        If <parameter>validate</parameter> is set to <literal>true</literal>,
+        then the function will perform a series of data consistency checks on
+        the data in <parameter>extended_stats</parameter> before attempting to
+        import statistics. Any inconsistencies found will raise an error.
+       </para></entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
-- 
2.43.0

From 101c6476df3b7e9d4f199b4c4a149e1dfeebd51c Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Thu, 1 Feb 2024 20:45:05 -0500
Subject: [PATCH v4 3/3] Add pg_export_stats, pg_import_stats

These are reference programs designed to aid the testing of the
functions pg_import_rel_stats() and pg_import_ext_stats(), bringing in
statistics from  older versions of postgresql.

The ultimate goal is to move the queries into pg_dump.
---
 src/bin/scripts/.gitignore        |    2 +
 src/bin/scripts/Makefile          |    4 +-
 src/bin/scripts/pg_export_stats.c | 1012 +++++++++++++++++++++++++++++
 src/bin/scripts/pg_import_stats.c |  477 ++++++++++++++
 4 files changed, 1494 insertions(+), 1 deletion(-)
 create mode 100644 src/bin/scripts/pg_export_stats.c
 create mode 100644 src/bin/scripts/pg_import_stats.c

diff --git a/src/bin/scripts/.gitignore b/src/bin/scripts/.gitignore
index 0f23fe0004..1b9addb339 100644
--- a/src/bin/scripts/.gitignore
+++ b/src/bin/scripts/.gitignore
@@ -6,5 +6,7 @@
 /reindexdb
 /vacuumdb
 /pg_isready
+/pg_export_stats
+/pg_import_stats
 
 /tmp_check/
diff --git a/src/bin/scripts/Makefile b/src/bin/scripts/Makefile
index 9633c99136..7550c69d81 100644
--- a/src/bin/scripts/Makefile
+++ b/src/bin/scripts/Makefile
@@ -16,7 +16,7 @@ subdir = src/bin/scripts
 top_builddir = ../../..
 include $(top_builddir)/src/Makefile.global
 
-PROGRAMS = createdb createuser dropdb dropuser clusterdb vacuumdb reindexdb pg_isready
+PROGRAMS = createdb createuser dropdb dropuser clusterdb vacuumdb reindexdb pg_isready pg_export_stats pg_import_stats
 
 override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS)
 LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport)
@@ -41,6 +41,8 @@ install: all installdirs
 	$(INSTALL_PROGRAM) vacuumdb$(X)   '$(DESTDIR)$(bindir)'/vacuumdb$(X)
 	$(INSTALL_PROGRAM) reindexdb$(X)  '$(DESTDIR)$(bindir)'/reindexdb$(X)
 	$(INSTALL_PROGRAM) pg_isready$(X) '$(DESTDIR)$(bindir)'/pg_isready$(X)
+	$(INSTALL_PROGRAM) pg_export_stats$(X) '$(DESTDIR)$(bindir)'/pg_export_stats$(X)
+	$(INSTALL_PROGRAM) pg_import_stats$(X) '$(DESTDIR)$(bindir)'/pg_import_stats$(X)
 
 installdirs:
 	$(MKDIR_P) '$(DESTDIR)$(bindir)'
diff --git a/src/bin/scripts/pg_export_stats.c b/src/bin/scripts/pg_export_stats.c
new file mode 100644
index 0000000000..c07450d1a9
--- /dev/null
+++ b/src/bin/scripts/pg_export_stats.c
@@ -0,0 +1,1012 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_export_stats
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/bin/scripts/pg_export_stats.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+#include "common.h"
+#include "common/logging.h"
+#include "fe_utils/cancel.h"
+#include "fe_utils/option_utils.h"
+#include "fe_utils/query_utils.h"
+#include "fe_utils/simple_list.h"
+#include "fe_utils/string_utils.h"
+
+static void help(const char *progname);
+
+/*
+ * Versions 12+ have the same rel stats layout
+ */
+const char *export_rel_query_v12 =
+	"SELECT "
+	"    n.nspname AS schemaname, "
+	"    r.relname AS relname, "
+	"    NULL::text AS ext_stats_name, "
+	"    current_setting('server_version_num')::integer AS server_version_num, "
+	"    jsonb_build_object( "
+	"        'server_version_num', current_setting('server_version_num'), "
+	"        'relname', r.relname, "
+	"        'nspname', n.nspname, "
+	"        'reltuples', r.reltuples, "
+	"        'relpages', r.relpages, "
+	"        'types', "
+	"        ( "
+	"            SELECT array_agg(tr ORDER BY tr.oid) "
+	"            FROM ( "
+	"                SELECT "
+	"                    t.oid, "
+	"                    t.typname, "
+	"                    n.nspname "
+	"                FROM pg_type AS t "
+	"                JOIN pg_namespace AS n ON n.oid = t.typnamespace "
+	"                WHERE t.oid IN ( "
+	"                    SELECT a.atttypid "
+	"                    FROM pg_attribute AS a "
+	"                    WHERE a.attrelid = r.oid "
+	"                    AND NOT a.attisdropped "
+	"                    AND a.attnum > 0 "
+	"                ) "
+	"            ) AS tr "
+	"        ), "
+	"        'collations', "
+	"        ( "
+	"            SELECT array_agg(cr ORDER BY cr.oid) "
+	"            FROM ( "
+	"                SELECT "
+	"                    c.oid, "
+	"                    c.collname, "
+	"                    n.nspname  "
+	"                FROM pg_collation AS c "
+	"                JOIN pg_namespace AS n ON n.oid = c.collnamespace "
+	"                WHERE c.oid IN ( "
+	"                    SELECT a.attcollation AS oid "
+	"                    FROM pg_attribute AS a "
+	"                    WHERE a.attrelid = r.oid "
+	"                    AND NOT a.attisdropped "
+	"                    AND a.attnum > 0 "
+	"                    UNION "
+	"                    SELECT u.collid "
+	"                    FROM pg_statistic AS s "
+	"                    CROSS JOIN LATERAL unnest(ARRAY[ "
+	"                                        s.stacoll1, s.stacoll2, "
+	"                                        s.stacoll3, s.stacoll4, "
+	"                                        s.stacoll5]) AS u(collid) "
+	"                    WHERE s.starelid = r.oid "
+	"                    ) "
+	"                ) AS cr "
+	"        ), "
+	"        'operators', "
+	"        ( "
+	"            SELECT array_agg(p ORDER BY p.oid) "
+	"            FROM ( "
+	"                SELECT "
+	"                    o.oid, "
+	"                    o.oprname, "
+	"                    n.nspname "
+	"                FROM pg_operator AS o "
+	"                JOIN pg_namespace AS n ON n.oid = o.oprnamespace "
+	"                WHERE o.oid IN ( "
+	"                    SELECT u.oid "
+	"                    FROM pg_statistic AS s "
+	"                    CROSS JOIN LATERAL unnest(ARRAY[ "
+	"                                        s.staop1, s.staop2, "
+	"                                        s.staop3, s.staop4, "
+	"                                        s.staop5]) AS u(opid) "
+	"                    WHERE s.starelid = r.oid "
+	"                    ) "
+	"            ) AS p "
+	"        ), "
+	"        'attributes',  "
+	"        ( "
+	"            SELECT array_agg(ar ORDER BY ar.attnum) "
+	"            FROM ( "
+	"                SELECT "
+	"                    a.attnum, "
+	"                    a.attname, "
+	"                    a.atttypid, "
+	"                    a.attcollation "
+	"                FROM pg_attribute AS a "
+	"                WHERE a.attrelid = r.oid "
+	"                AND NOT a.attisdropped "
+	"                AND a.attnum > 0 "
+	"            ) AS ar "
+	"        ), "
+	"        'statistics', "
+	"        ( "
+	"            SELECT array_agg(sr ORDER BY sr.stainherit, sr.staattnum) "
+	"            FROM ( "
+	"                SELECT "
+	"                     s.staattnum, "
+	"                     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::text AS stanumbers1, "
+	"                     s.stanumbers2::text AS stanumbers2, "
+	"                     s.stanumbers3::text AS stanumbers3, "
+	"                     s.stanumbers4::text AS stanumbers4, "
+	"                     s.stanumbers5::text AS stanumbers5, "
+	"                     s.stavalues1::text AS stavalues1, "
+	"                     s.stavalues2::text AS stavalues2, "
+	"                     s.stavalues3::text AS stavalues3, "
+	"                     s.stavalues4::text AS stavalues4, "
+	"                     s.stavalues5::text AS stavalues5 "
+	"                FROM pg_statistic AS s "
+	"                WHERE s.starelid = r.oid "
+	"            ) AS sr "
+	"        ) "
+	"    ) AS stats_json "
+	"FROM pg_class AS r "
+	"JOIN pg_namespace AS n ON n.oid = r.relnamespace "
+	"WHERE r.relkind IN ('r', 'm', 'f', 'p', 'i') "
+	"AND r.relpersistence = 'p' "
+	"AND n.nspname NOT IN ('pg_catalog', 'pg_toast', 'information_schema') ";
+
+/*
+ * Versions 10-11 are missing the pg_statistic.stacollN columns
+ */
+const char *export_rel_query_v10 =
+	"SELECT "
+	"    n.nspname AS schemaname, "
+	"    r.relname AS relname, "
+	"    NULL::text AS ext_stats_name, "
+	"    current_setting('server_version_num')::integer AS server_version_num, "
+	"    jsonb_build_object( "
+	"        'types', "
+	"        ( "
+	"            SELECT array_agg(tr ORDER BY tr.oid) "
+	"            FROM ( "
+	"                SELECT "
+	"                    t.oid, "
+	"                    t.typname, "
+	"                    n.nspname "
+	"                FROM pg_type AS t "
+	"                JOIN pg_namespace AS n ON n.oid = t.typnamespace "
+	"                WHERE t.oid IN ( "
+	"                    SELECT a.atttypid "
+	"                    FROM pg_attribute AS a "
+	"                    WHERE a.attrelid = r.oid "
+	"                    AND NOT a.attisdropped "
+	"                    AND a.attnum > 0 "
+	"                ) "
+	"            ) AS tr "
+	"        ), "
+	"        'collations', "
+	"        ( "
+	"            SELECT array_agg(cr ORDER BY cr.oid) "
+	"            FROM ( "
+	"                SELECT "
+	"                    c.oid, "
+	"                    c.collname, "
+	"                    n.nspname  "
+	"                FROM pg_collation AS c "
+	"                JOIN pg_namespace AS n ON n.oid = c.collnamespace "
+	"                WHERE c.oid IN ( "
+	"                    SELECT a.attcollation AS oid "
+	"                    FROM pg_attribute AS a "
+	"                    WHERE a.attrelid = r.oid "
+	"                    AND NOT a.attisdropped "
+	"                    AND a.attnum > 0 "
+	"                    ) "
+	"                ) AS cr "
+	"        ), "
+	"        'operators', "
+	"        ( "
+	"            SELECT array_agg(p ORDER BY p.oid) "
+	"            FROM ( "
+	"                SELECT "
+	"                    o.oid, "
+	"                    o.oprname, "
+	"                    n.nspname "
+	"                FROM pg_operator AS o "
+	"                JOIN pg_namespace AS n ON n.oid = o.oprnamespace "
+	"                WHERE o.oid IN ( "
+	"                    SELECT u.oid "
+	"                    FROM pg_statistic AS s "
+	"                    CROSS JOIN LATERAL unnest(ARRAY[ "
+	"                                        s.staop1, s.staop2, "
+	"                                        s.staop3, s.staop4, "
+	"                                        s.staop5]) AS u(opid) "
+	"                    WHERE s.starelid = r.oid "
+	"                    ) "
+	"            ) AS p "
+	"        ), "
+	"        'attributes',  "
+	"        ( "
+	"            SELECT array_agg(ar ORDER BY ar.attnum) "
+	"            FROM ( "
+	"                SELECT "
+	"                    a.attnum, "
+	"                    a.attname, "
+	"                    a.atttypid, "
+	"                    a.attcollation "
+	"                FROM pg_attribute AS a "
+	"                WHERE a.attrelid = r.oid "
+	"                AND NOT a.attisdropped "
+	"                AND a.attnum > 0 "
+	"            ) AS ar "
+	"        ), "
+	"        'statistics', "
+	"        ( "
+	"            SELECT array_agg(sr ORDER BY sr.stainherit, sr.staattnum) "
+	"            FROM ( "
+	"                SELECT "
+	"                     s.staattnum, "
+	"                     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.stanumbers1::text AS stanumbers1, "
+	"                     s.stanumbers2::text AS stanumbers2, "
+	"                     s.stanumbers3::text AS stanumbers3, "
+	"                     s.stanumbers4::text AS stanumbers4, "
+	"                     s.stanumbers5::text AS stanumbers5, "
+	"                     s.stavalues1::text AS stavalues1, "
+	"                     s.stavalues2::text AS stavalues2, "
+	"                     s.stavalues3::text AS stavalues3, "
+	"                     s.stavalues4::text AS stavalues4, "
+	"                     s.stavalues5::text AS stavalues5 "
+	"                FROM pg_statistic AS s "
+	"                WHERE s.starelid = r.oid "
+	"            ) AS sr "
+	"        ) "
+	"    ) AS stats_json "
+	"FROM pg_class AS r "
+	"JOIN pg_namespace AS n ON n.oid = r.relnamespace "
+	"WHERE r.relkind IN ('r', 'm', 'f', 'p', 'i') "
+	"AND r.relpersistence = 'p' "
+	"AND n.nspname NOT IN ('pg_catalog', 'pg_toast', 'information_schema') ";
+
+
+const char *export_ext_query_v15 =
+/* v15+ have the same format */
+	"SELECT "
+	"    n.nspname AS schemaname, "
+	"    r.relname AS relname, "
+	"    e.stxname AS ext_stats_name, "
+	"    (current_setting('server_version_num'::text))::integer AS server_version_num, "
+	"    jsonb_build_object( "
+	"        'server_version_num', current_setting('server_version_num'), "
+	"        'stxoid', e.oid, "
+	"        'reloid', r.oid, "
+	"        'stxname', e.stxname, "
+	"        'stxnspname', en.nspname, "
+	"        'relname', r.relname, "
+	"        'nspname', n.nspname, "
+	"        'stxkeys', e.stxkeys::text, "
+	"        'stxkind', e.stxkind::text, "
+	"        'data', "
+	"        ( "
+	"            SELECT array_agg(dr ORDER by dr.stxdinherit) "
+	"            FROM ( "
+	"                SELECT "
+	"                    sd.stxdinherit, "
+	"                    sd.stxdndistinct::text AS stxdndistinct,  "
+	"                    sd.stxddependencies::text AS stxddependencies,  "
+	"                    ( "
+	"                        SELECT array_agg(mcvl) "
+	"                        FROM pg_mcv_list_items(sd.stxdmcv) AS mcvl "
+	"                        WHERE sd.stxdmcv IS NOT NULL "
+	"                    ) AS stxdmcv, "
+	"                    ( "
+	"                        SELECT array_agg(sr ORDER BY sr.stainherit, sr.staattnum) "
+	"                        FROM ( "
+	"                            SELECT "
+	"                                 s.staattnum, "
+	"                                 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::text AS stanumbers1, "
+	"                                 s.stanumbers2::text AS stanumbers2, "
+	"                                 s.stanumbers3::text AS stanumbers3, "
+	"                                 s.stanumbers4::text AS stanumbers4, "
+	"                                 s.stanumbers5::text AS stanumbers5, "
+	"                                 s.stavalues1::text AS stavalues1, "
+	"                                 s.stavalues2::text AS stavalues2, "
+	"                                 s.stavalues3::text AS stavalues3, "
+	"                                 s.stavalues4::text AS stavalues4, "
+	"                                 s.stavalues5::text AS stavalues5 "
+	"                            FROM unnest(sd.stxdexpr) AS s "
+	"                            WHERE sd.stxdexpr IS NOT NULL "
+	"                        ) AS sr "
+	"                    ) AS stxdexpr "
+	"                FROM pg_statistic_ext_data AS sd "
+	"                WHERE sd.stxoid = e.oid "
+	"            ) AS dr "
+	"        ), "
+	"        'types', "
+	"        ( "
+	"            SELECT array_agg(tr ORDER BY tr.oid) "
+	"            FROM ( "
+	"                SELECT "
+	"                    t.oid, "
+	"                    t.typname, "
+	"                    n.nspname "
+	"                FROM pg_type AS t "
+	"                JOIN pg_namespace AS n ON n.oid = t.typnamespace "
+	"                WHERE t.oid IN ( "
+	"                    SELECT a.atttypid "
+	"                    FROM pg_attribute AS a "
+	"                    WHERE a.attrelid = r.oid "
+	"                    AND NOT a.attisdropped "
+	"                    AND a.attnum > 0 "
+	"                ) "
+	"            ) AS tr "
+	"        ), "
+	"        'collations', "
+	"        ( "
+	"            SELECT array_agg(cr ORDER BY cr.oid) "
+	"            FROM ( "
+	"                SELECT "
+	"                    c.oid, "
+	"                    c.collname, "
+	"                    n.nspname  "
+	"                FROM pg_collation AS c "
+	"                JOIN pg_namespace AS n ON n.oid = c.collnamespace "
+	"                WHERE c.oid IN ( "
+	"                    SELECT a.attcollation AS oid "
+	"                    FROM pg_attribute AS a "
+	"                    WHERE a.attrelid = r.oid "
+	"                    AND NOT a.attisdropped "
+	"                    AND a.attnum > 0 "
+	"                    UNION "
+	"                    SELECT u.collid "
+	"                    FROM pg_statistic_ext_data AS sd "
+	"                    CROSS JOIN LATERAL unnest(sd.stxdexpr) AS s "
+	"                    CROSS JOIN LATERAL unnest(ARRAY[ "
+	"                                        s.stacoll1, s.stacoll2, "
+	"                                        s.stacoll3, s.stacoll4, "
+	"                                        s.stacoll5]) AS u(collid) "
+	"                    WHERE sd.stxoid = e.oid "
+	"                    AND sd.stxdexpr IS NOT NULL "
+	"                    ) "
+	"                ) AS cr "
+	"        ), "
+	"        'operators', "
+	"        ( "
+	"            SELECT array_agg(p ORDER BY p.oid) "
+	"            FROM ( "
+	"                SELECT "
+	"                    o.oid, "
+	"                    o.oprname, "
+	"                    n.nspname "
+	"                FROM pg_operator AS o "
+	"                JOIN pg_namespace AS n ON n.oid = o.oprnamespace "
+	"                WHERE o.oid IN ( "
+	"                    SELECT u.opid "
+	"                    FROM pg_statistic_ext_data AS sd "
+	"                    CROSS JOIN LATERAL unnest(sd.stxdexpr) AS s "
+	"                    CROSS JOIN LATERAL unnest(ARRAY[ "
+	"                                        s.staop1, s.staop2, "
+	"                                        s.staop3, s.staop4, "
+	"                                        s.staop5]) AS u(opid) "
+	"                    WHERE sd.stxoid = e.oid "
+	"                    AND sd.stxdexpr IS NOT NULL "
+	"                    ) "
+	"            ) AS p "
+	"        ), "
+	"        'attributes',  "
+	"        ( "
+	"            SELECT array_agg(ar ORDER BY ar.attnum) "
+	"            FROM ( "
+	"                SELECT "
+	"                    a.attnum, "
+	"                    a.attname, "
+	"                    a.atttypid, "
+	"                    a.attcollation "
+	"                FROM pg_attribute AS a "
+	"                WHERE a.attrelid = r.oid "
+	"                AND NOT a.attisdropped "
+	"                AND a.attnum > 0 "
+	"            ) AS ar "
+	"        ) "
+	"    ) AS ext_stats_json "
+	"FROM pg_class r "
+	"JOIN pg_statistic_ext AS e ON e.stxrelid = r.oid "
+	"JOIN pg_namespace AS en ON en.oid = e.stxnamespace "
+	"JOIN pg_namespace AS n ON n.oid = r.relnamespace "
+	"WHERE r.relkind IN ('r', 'm', 'f', 'p', 'i') "
+	"AND r.relpersistence = 'p' "
+	"AND n.nspname NOT IN ('pg_catalog', 'pg_toast', 'information_schema') ";
+
+/* v14 is like v15, but lacks stxdinherit on pg_statistic_ext_data */
+const char *export_ext_query_v14 =
+	"SELECT "
+	"    n.nspname AS schemaname, "
+	"    r.relname AS relname, "
+	"    e.stxname AS ext_stats_name, "
+	"    (current_setting('server_version_num'::text))::integer AS server_version_num, "
+	"    jsonb_build_object( "
+	"        'server_version_num', current_setting('server_version_num'), "
+	"        'stxoid', e.oid, "
+	"        'reloid', r.oid, "
+	"        'stxname', e.stxname, "
+	"        'stxnspname', en.nspname, "
+	"        'relname', r.relname, "
+	"        'nspname', n.nspname, "
+	"        'stxkeys', e.stxkeys::text, "
+	"        'stxkind', e.stxkind::text, "
+	"        'data', "
+	"        ( "
+	"            SELECT array_agg(dr) "
+	"            FROM ( "
+	"                SELECT "
+	"                    sd.stxdndistinct::text AS stxdndistinct,  "
+	"                    sd.stxddependencies::text AS stxddependencies,  "
+	"                    ( "
+	"                        SELECT array_agg(mcvl) "
+	"                        FROM pg_mcv_list_items(sd.stxdmcv) AS mcvl "
+	"                        WHERE sd.stxdmcv IS NOT NULL "
+	"                    ) AS stxdmcv, "
+	"                    ( "
+	"                        SELECT array_agg(sr ORDER BY sr.staattnum) "
+	"                        FROM ( "
+	"                            SELECT "
+	"                                 s.staattnum, "
+	"                                 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::text AS stanumbers1, "
+	"                                 s.stanumbers2::text AS stanumbers2, "
+	"                                 s.stanumbers3::text AS stanumbers3, "
+	"                                 s.stanumbers4::text AS stanumbers4, "
+	"                                 s.stanumbers5::text AS stanumbers5, "
+	"                                 s.stavalues1::text AS stavalues1, "
+	"                                 s.stavalues2::text AS stavalues2, "
+	"                                 s.stavalues3::text AS stavalues3, "
+	"                                 s.stavalues4::text AS stavalues4, "
+	"                                 s.stavalues5::text AS stavalues5 "
+	"                            FROM unnest(sd.stxdexpr) AS s "
+	"                            WHERE sd.stxdexpr IS NOT NULL "
+	"                        ) AS sr "
+	"                    ) AS stxdexpr "
+	"                FROM pg_statistic_ext_data AS sd "
+	"                WHERE sd.stxoid = e.oid "
+	"            ) dr "
+	"        ), "
+	"        'types', "
+	"        ( "
+	"            select array_agg(tr ORDER BY tr.oid) "
+	"            from ( "
+	"                select "
+	"                    t.oid, "
+	"                    t.typname, "
+	"                    n.nspname "
+	"                from pg_type as t "
+	"                join pg_namespace as n on n.oid = t.typnamespace "
+	"                where t.oid in ( "
+	"                    select a.atttypid "
+	"                    from pg_attribute as a "
+	"                    where a.attrelid = r.oid "
+	"                    and not a.attisdropped "
+	"                    and a.attnum > 0 "
+	"                ) "
+	"            ) as tr "
+	"        ), "
+	"        'collations', "
+	"        ( "
+	"            SELECT array_agg(cr ORDER BY cr.oid) "
+	"            FROM ( "
+	"                SELECT "
+	"                    c.oid, "
+	"                    c.collname, "
+	"                    n.nspname  "
+	"                FROM pg_collation AS c "
+	"                JOIN pg_namespace AS n ON n.oid = c.collnamespace "
+	"                WHERE c.oid IN ( "
+	"                    SELECT a.attcollation AS oid "
+	"                    FROM pg_attribute AS a "
+	"                    WHERE a.attrelid = r.oid "
+	"                    AND NOT a.attisdropped "
+	"                    AND a.attnum > 0 "
+	"                    UNION "
+	"                    SELECT u.collid "
+	"                    FROM pg_statistic_ext_data AS sd "
+	"                    CROSS JOIN LATERAL unnest(sd.stxdexpr) AS s "
+	"                    CROSS JOIN LATERAL unnest(ARRAY[ "
+	"                                        s.stacoll1, s.stacoll2, "
+	"                                        s.stacoll3, s.stacoll4, "
+	"                                        s.stacoll5]) AS u(collid) "
+	"                    WHERE sd.stxoid = e.oid "
+	"                    AND sd.stxdexpr IS NOT NULL "
+	"                    ) "
+	"                ) AS cr "
+	"        ), "
+	"        'operators', "
+	"        ( "
+	"            SELECT array_agg(p ORDER BY p.oid) "
+	"            FROM ( "
+	"                SELECT "
+	"                    o.oid, "
+	"                    o.oprname, "
+	"                    n.nspname "
+	"                FROM pg_operator AS o "
+	"                JOIN pg_namespace AS n ON n.oid = o.oprnamespace "
+	"                WHERE o.oid IN ( "
+	"                    SELECT u.opid "
+	"                    FROM pg_statistic_ext_data AS sd "
+	"                    CROSS JOIN LATERAL unnest(sd.stxdexpr) AS s "
+	"                    CROSS JOIN LATERAL unnest(ARRAY[ "
+	"                                        s.staop1, s.staop2, "
+	"                                        s.staop3, s.staop4, "
+	"                                        s.staop5]) AS u(opid) "
+	"                    WHERE sd.stxoid = e.oid "
+	"                    AND sd.stxdexpr IS NOT NULL "
+	"                    ) "
+	"            ) AS p "
+	"        ), "
+	"        'attributes',  "
+	"        ( "
+	"            SELECT array_agg(ar ORDER BY ar.attnum) "
+	"            FROM ( "
+	"                SELECT "
+	"                    a.attnum, "
+	"                    a.attname, "
+	"                    a.atttypid, "
+	"                    a.attcollation "
+	"                FROM pg_attribute AS a "
+	"                WHERE a.attrelid = r.oid "
+	"                AND NOT a.attisdropped "
+	"                AND a.attnum > 0 "
+	"            ) AS ar "
+	"        ) "
+	"    ) AS ext_stats_json "
+	"FROM pg_class r "
+	"JOIN pg_statistic_ext AS e ON e.stxrelid = r.oid "
+	"JOIN pg_namespace AS en ON en.oid = e.stxnamespace "
+	"JOIN pg_namespace AS n ON n.oid = r.relnamespace "
+	"WHERE r.relkind IN ('r', 'm', 'f', 'p', 'i') "
+	"AND r.relpersistence = 'p' "
+	"AND n.nspname NOT IN ('pg_catalog', 'pg_toast', 'information_schema') ";
+
+/* v12-v13 are like v14, but lack stxdexpr on pg_statistic_ext_data */
+const char *export_ext_query_v12 =
+	"SELECT "
+	"    n.nspname AS schemaname, "
+	"    r.relname AS relname, "
+	"    e.stxname AS ext_stats_name, "
+	"    (current_setting('server_version_num'::text))::integer AS server_version_num, "
+	"    jsonb_build_object( "
+	"        'server_version_num', current_setting('server_version_num'), "
+	"        'stxoid', e.oid, "
+	"        'reloid', r.oid, "
+	"        'stxname', e.stxname, "
+	"        'stxnspname', en.nspname, "
+	"        'relname', r.relname, "
+	"        'nspname', n.nspname, "
+	"        'stxkeys', e.stxkeys::text, "
+	"        'stxkind', e.stxkind::text, "
+	"        'data', "
+	"        ( "
+	"            SELECT array_agg(r) "
+	"            FROM ( "
+	"                SELECT "
+	"                    sd.stxdndistinct::text AS stxdndistinct,  "
+	"                    sd.stxddependencies::text AS stxddependencies,  "
+	"                    ( "
+	"                        SELECT array_agg(mcvl) "
+	"                        FROM pg_mcv_list_items(sd.stxdmcv) AS mcvl "
+	"                        WHERE sd.stxdmcv IS NOT NULL "
+	"                    ) AS stxdmcv "
+	"                FROM pg_statistic_ext_data AS sd "
+	"                WHERE sd.stxoid = e.oid "
+	"            ) r "
+	"        ), "
+	"        'types', "
+	"        ( "
+	"            SELECT array_agg(tr ORDER BY tr.oid) "
+	"            FROM ( "
+	"                SELECT "
+	"                    t.oid, "
+	"                    t.typname, "
+	"                    n.nspname "
+	"                FROM pg_type AS t "
+	"                JOIN pg_namespace AS n ON n.oid = t.typnamespace "
+	"                WHERE t.oid IN ( "
+	"                    SELECT a.atttypid "
+	"                    FROM pg_attribute AS a "
+	"                    WHERE a.attrelid = r.oid "
+	"                    AND NOT a.attisdropped "
+	"                    AND a.attnum > 0 "
+	"                ) "
+	"            ) AS tr "
+	"        ), "
+	"        'collations', "
+	"        ( "
+	"            SELECT array_agg(cr ORDER BY cr.oid) "
+	"            FROM ( "
+	"                SELECT "
+	"                    c.oid, "
+	"                    c.collname, "
+	"                    n.nspname  "
+	"                FROM pg_collation AS c "
+	"                JOIN pg_namespace AS n ON n.oid = c.collnamespace "
+	"                WHERE c.oid IN ( "
+	"                    SELECT a.attcollation AS oid "
+	"                    FROM pg_attribute AS a "
+	"                    WHERE a.attrelid = r.oid "
+	"                    AND NOT a.attisdropped "
+	"                    AND a.attnum > 0 "
+	"                    ) "
+	"                ) AS cr "
+	"        ), "
+	"        'attributes',  "
+	"        ( "
+	"            SELECT array_agg(ar ORDER BY ar.attnum) "
+	"            FROM ( "
+	"                SELECT "
+	"                    a.attnum, "
+	"                    a.attname, "
+	"                    a.atttypid, "
+	"                    a.attcollation "
+	"                FROM pg_attribute AS a "
+	"                WHERE a.attrelid = r.oid "
+	"                AND NOT a.attisdropped "
+	"                AND a.attnum > 0 "
+	"            ) AS ar "
+	"        ) "
+	"    ) AS ext_stats_json "
+	"FROM pg_class r "
+	"JOIN pg_statistic_ext AS e ON e.stxrelid = r.oid "
+	"JOIN pg_namespace AS en ON en.oid = e.stxnamespace "
+	"JOIN pg_namespace AS n ON n.oid = r.relnamespace "
+	"WHERE r.relkind IN ('r', 'm', 'f', 'p', 'i') "
+	"AND r.relpersistence = 'p' "
+	"AND n.nspname NOT IN ('pg_catalog', 'pg_toast', 'information_schema') ";
+
+/*
+ * v10-v11 are like v12, but:
+ *     - MCV is gone
+ *     - remaining stats are stored on pg_statistic_ext
+ *     - pg_statistic_ext_data is gone
+ */
+
+const char *export_ext_query_v10 =
+	"SELECT "
+	"    n.nspname AS schemaname, "
+	"    r.relname AS relname, "
+	"    e.stxname AS ext_stats_name, "
+	"    (current_setting('server_version_num'::text))::integer AS server_version_num, "
+	"    jsonb_build_object( "
+	"        'server_version_num', current_setting('server_version_num'), "
+	"        'stxoid', e.oid, "
+	"        'reloid', r.oid, "
+	"        'stxname', e.stxname, "
+	"        'stxnspname', en.nspname, "
+	"        'relname', r.relname, "
+	"        'nspname', n.nspname, "
+	"        'stxkeys', e.stxkeys::text, "
+	"        'stxkind', e.stxkind::text, "
+	"        'stxndistinct', e.stxndistinct::text, "
+	"        'stxdependencies', e.stxdependencies::text, "
+	"        'types', "
+	"        ( "
+	"            SELECT array_agg(tr ORDER BY tr.oid) "
+	"            FROM ( "
+	"                SELECT "
+	"                    t.oid, "
+	"                    t.typname, "
+	"                    n.nspname "
+	"                FROM pg_type AS t "
+	"                JOIN pg_namespace AS n ON n.oid = t.typnamespace "
+	"                WHERE t.oid IN ( "
+	"                    SELECT a.atttypid "
+	"                    FROM pg_attribute AS a "
+	"                    WHERE a.attrelid = r.oid "
+	"                    AND NOT a.attisdropped "
+	"                    AND a.attnum > 0 "
+	"                ) "
+	"            ) AS tr "
+	"        ), "
+	"        'collations', "
+	"        ( "
+	"            SELECT array_agg(cr ORDER BY cr.oid) "
+	"            FROM ( "
+	"                SELECT "
+	"                    c.oid, "
+	"                    c.collname, "
+	"                    n.nspname  "
+	"                FROM pg_collation AS c "
+	"                JOIN pg_namespace AS n ON n.oid = c.collnamespace "
+	"                WHERE c.oid IN ( "
+	"                    SELECT a.attcollation AS oid "
+	"                    FROM pg_attribute AS a "
+	"                    WHERE a.attrelid = r.oid "
+	"                    AND NOT a.attisdropped "
+	"                    AND a.attnum > 0 "
+	"                    ) "
+	"                ) AS cr "
+	"        ), "
+	"        'attributes',  "
+	"        ( "
+	"            SELECT array_agg(ar ORDER BY r.attnum) "
+	"            FROM ( "
+	"                SELECT "
+	"                    a.attnum, "
+	"                    a.attname, "
+	"                    a.atttypid, "
+	"                    a.attcollation "
+	"                FROM pg_attribute AS a "
+	"                WHERE a.attrelid = r.oid "
+	"                AND NOT a.attisdropped "
+	"                AND a.attnum > 0 "
+	"            ) AS ar "
+	"        ) "
+	"    ) AS ext_stats_json "
+	"FROM pg_class r "
+	"JOIN pg_statistic_ext AS e ON e.stxrelid = r.oid "
+	"JOIN pg_namespace AS en ON en.oid = e.stxnamespace "
+	"JOIN pg_namespace AS n ON n.oid = r.relnamespace "
+	"WHERE r.relkind IN ('r', 'm', 'f', 'p', 'i') "
+	"AND r.relpersistence = 'p' "
+	"AND n.nspname NOT IN ('pg_catalog', 'pg_toast', 'information_schema') ";
+
+
+
+int
+main(int argc, char *argv[])
+{
+	static struct option long_options[] = {
+		{"host", required_argument, NULL, 'h'},
+		{"port", required_argument, NULL, 'p'},
+		{"username", required_argument, NULL, 'U'},
+		{"no-password", no_argument, NULL, 'w'},
+		{"password", no_argument, NULL, 'W'},
+		{"echo", no_argument, NULL, 'e'},
+		{"dbname", required_argument, NULL, 'd'},
+		{NULL, 0, NULL, 0}
+	};
+
+	const char *progname;
+	int			optindex;
+	int			c;
+
+	const char *dbname = NULL;
+	char	   *host = NULL;
+	char	   *port = NULL;
+	char	   *username = NULL;
+	enum trivalue prompt_password = TRI_DEFAULT;
+	ConnParams	cparams;
+	bool		echo = false;
+
+	PQExpBufferData sql;
+
+	PGconn	   *conn;
+	int			server_version_num;
+
+	FILE	   *copystream = stdout;
+
+	PGresult   *result;
+
+	ExecStatusType result_status;
+
+	char	   *buf;
+	int			ret;
+
+	pg_logging_init(argv[0]);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pgscripts"));
+
+	handle_help_version_opts(argc, argv, "clusterdb", help);
+
+	while ((c = getopt_long(argc, argv, "d:eh:p:U:wW", long_options, &optindex)) != -1)
+	{
+		switch (c)
+		{
+			case 'd':
+				dbname = pg_strdup(optarg);
+				break;
+			case 'e':
+				echo = true;
+				break;
+			case 'h':
+				host = pg_strdup(optarg);
+				break;
+			case 'p':
+				port = pg_strdup(optarg);
+				break;
+			case 'U':
+				username = pg_strdup(optarg);
+				break;
+			case 'w':
+				prompt_password = TRI_NO;
+				break;
+			case 'W':
+				prompt_password = TRI_YES;
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Non-option argument specifies database name as long as it wasn't
+	 * already specified with -d / --dbname
+	 */
+	if (optind < argc && dbname == NULL)
+	{
+		dbname = argv[optind];
+		optind++;
+	}
+
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/* fill cparams except for dbname, which is set below */
+	cparams.pghost = host;
+	cparams.pgport = port;
+	cparams.pguser = username;
+	cparams.prompt_password = prompt_password;
+	cparams.override_dbname = NULL;
+
+	setup_cancel_handler(NULL);
+
+	if (dbname == NULL)
+	{
+		if (getenv("PGDATABASE"))
+			dbname = getenv("PGDATABASE");
+		else if (getenv("PGUSER"))
+			dbname = getenv("PGUSER");
+		else
+			dbname = get_user_name_or_exit(progname);
+	}
+
+	cparams.dbname = dbname;
+
+	conn = connectDatabase(&cparams, progname, echo, false, true);
+
+	server_version_num = PQserverVersion(conn);
+
+	initPQExpBuffer(&sql);
+
+	appendPQExpBufferStr(&sql, "COPY (");
+
+	if (server_version_num >= 120000)
+		appendPQExpBufferStr(&sql, export_rel_query_v12);
+	else if (server_version_num >= 100000)
+		appendPQExpBufferStr(&sql, export_rel_query_v10);
+	else
+		pg_fatal("exporting statistics from databases prior to version 10 not supported");
+
+	appendPQExpBufferStr(&sql, " UNION ALL ");
+
+	if (server_version_num >= 150000)
+		appendPQExpBufferStr(&sql, export_ext_query_v15);
+	else if (server_version_num >= 140000)
+		appendPQExpBufferStr(&sql, export_ext_query_v14);
+	else if (server_version_num >= 120000)
+		appendPQExpBufferStr(&sql, export_ext_query_v12);
+	else if (server_version_num >= 100000)
+		appendPQExpBufferStr(&sql, export_ext_query_v10);
+	else
+		pg_fatal("exporting statistics from databases prior to version 10 not supported");
+
+	appendPQExpBufferStr(&sql, " ORDER BY 1, 2, 3) TO STDOUT");
+ 
+	/* printf("%s\n", sql.data); */
+	result = PQexec(conn, sql.data);
+	result_status = PQresultStatus(result);
+
+	if (result_status != PGRES_COPY_OUT)
+		pg_fatal("malformed copy command: %s", PQerrorMessage(conn));
+
+	for (;;)
+	{
+		ret = PQgetCopyData(conn, &buf, 0);
+
+		if (ret < 0)
+			break;				/* done or server/connection error */
+
+		if (buf)
+		{
+			if (copystream && fwrite(buf, 1, ret, copystream) != ret)
+				pg_fatal("could not write COPY data: %m");
+			PQfreemem(buf);
+		}
+	}
+
+	if (copystream && fflush(copystream))
+		pg_fatal("could not write COPY data: %m");
+
+	if (ret == -2)
+		pg_fatal("COPY data transfer failed: %s", PQerrorMessage(conn));
+
+	PQfinish(conn);
+	termPQExpBuffer(&sql);
+	exit(0);
+}
+
+
+static void
+help(const char *progname)
+{
+	printf(_("%s clusters all previously clustered tables in a database.\n\n"), progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]... [DBNAME]\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_("  -d, --dbname=DBNAME       database to cluster\n"));
+	printf(_("  -e, --echo                show the commands being sent to the server\n"));
+	printf(_("  -t, --table=TABLE         cluster specific table(s) only\n"));
+	printf(_("  -V, --version             output version information, then exit\n"));
+	printf(_("  -?, --help                show this help, then exit\n"));
+	printf(_("\nConnection options:\n"));
+	printf(_("  -h, --host=HOSTNAME       database server host or socket directory\n"));
+	printf(_("  -p, --port=PORT           database server port\n"));
+	printf(_("  -U, --username=USERNAME   user name to connect as\n"));
+	printf(_("  -w, --no-password         never prompt for password\n"));
+	printf(_("  -W, --password            force password prompt\n"));
+	printf(_("  --maintenance-db=DBNAME   alternate maintenance database\n"));
+	printf(_("\nRead the description of the SQL command CLUSTER for details.\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
diff --git a/src/bin/scripts/pg_import_stats.c b/src/bin/scripts/pg_import_stats.c
new file mode 100644
index 0000000000..96a7252fec
--- /dev/null
+++ b/src/bin/scripts/pg_import_stats.c
@@ -0,0 +1,477 @@
+/*-------------------------------------------------------------------------
+ *
+ * pg_import_stats
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/bin/scripts/pg_import_stats.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+#include "common.h"
+#include "common/logging.h"
+#include "fe_utils/cancel.h"
+#include "fe_utils/option_utils.h"
+#include "fe_utils/query_utils.h"
+#include "fe_utils/simple_list.h"
+#include "fe_utils/string_utils.h"
+
+#define COPY_BUF_LEN 8192
+
+static void help(const char *progname);
+
+int
+main(int argc, char *argv[])
+{
+	static struct option long_options[] = {
+		{"host", required_argument, NULL, 'h'},
+		{"port", required_argument, NULL, 'p'},
+		{"username", required_argument, NULL, 'U'},
+		{"no-password", no_argument, NULL, 'w'},
+		{"password", no_argument, NULL, 'W'},
+		{"quiet", no_argument, NULL, 'q'},
+		{"dbname", required_argument, NULL, 'd'},
+		{NULL, 0, NULL, 0}
+	};
+
+	const char *progname;
+	int			optindex;
+	int			c;
+
+	const char *dbname = NULL;
+	char	   *host = NULL;
+	char	   *port = NULL;
+	char	   *username = NULL;
+	enum trivalue prompt_password = TRI_DEFAULT;
+	ConnParams	cparams;
+	bool		quiet = false;
+
+	PGconn	   *conn;
+
+	FILE	   *copysrc= stdin;
+
+	PGresult   *result;
+
+	int		i;
+	int		numtables;
+	int		numextstats;
+
+	pg_logging_init(argv[0]);
+	progname = get_progname(argv[0]);
+	set_pglocale_pgservice(argv[0], PG_TEXTDOMAIN("pgscripts"));
+
+	handle_help_version_opts(argc, argv, "clusterdb", help);
+
+	while ((c = getopt_long(argc, argv, "d:h:p:qU:wW", long_options, &optindex)) != -1)
+	{
+		switch (c)
+		{
+			case 'd':
+				dbname = pg_strdup(optarg);
+				break;
+			case 'h':
+				host = pg_strdup(optarg);
+				break;
+			case 'p':
+				port = pg_strdup(optarg);
+				break;
+			case 'q':
+				quiet = true;
+				break;
+			case 'U':
+				username = pg_strdup(optarg);
+				break;
+			case 'w':
+				prompt_password = TRI_NO;
+				break;
+			case 'W':
+				prompt_password = TRI_YES;
+				break;
+			default:
+				/* getopt_long already emitted a complaint */
+				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+				exit(1);
+		}
+	}
+
+	/*
+	 * Non-option argument specifies database name as long as it wasn't
+	 * already specified with -d / --dbname
+	 */
+	if (optind < argc && dbname == NULL)
+	{
+		dbname = argv[optind];
+		optind++;
+	}
+
+	if (optind < argc)
+	{
+		pg_log_error("too many command-line arguments (first is \"%s\")",
+					 argv[optind]);
+		pg_log_error_hint("Try \"%s --help\" for more information.", progname);
+		exit(1);
+	}
+
+	/* fill cparams except for dbname, which is set below */
+	cparams.pghost = host;
+	cparams.pgport = port;
+	cparams.pguser = username;
+	cparams.prompt_password = prompt_password;
+	cparams.override_dbname = NULL;
+
+	setup_cancel_handler(NULL);
+
+	if (dbname == NULL)
+	{
+		if (getenv("PGDATABASE"))
+			dbname = getenv("PGDATABASE");
+		else if (getenv("PGUSER"))
+			dbname = getenv("PGUSER");
+		else
+			dbname = get_user_name_or_exit(progname);
+	}
+
+	cparams.dbname = dbname;
+
+	conn = connectDatabase(&cparams, progname, false, false, true);
+
+	/* open file */
+
+	/* iterate over records */
+
+	/*
+	 * Create a table that can received the COPY-ed file which is a mix
+	 * of relation statistics and extended statistics.
+	 */
+	result = PQexec(conn,
+		"CREATE TEMPORARY TABLE import_stats ( "
+		"schemaname text, "
+		"relname text, "
+		"ext_stats_name text, "
+		"server_version_num integer, "
+		"stats jsonb )");
+
+	if (PQresultStatus(result) != PGRES_COMMAND_OK)
+		pg_fatal("could not create temporary table: %s", PQerrorMessage(conn));
+
+	PQclear(result);
+
+	/*
+	 * Create a table just for the relation statistics
+	 */
+	result = PQexec(conn,
+		"CREATE TEMPORARY TABLE import_rel_stats ( "
+		"id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, "
+		"schemaname text, "
+		"relname text, "
+		"server_version_num integer, "
+		"stats jsonb )");
+
+	if (PQresultStatus(result) != PGRES_COMMAND_OK)
+		pg_fatal("could not create temporary table: %s", PQerrorMessage(conn));
+
+
+	PQclear(result);
+
+	/*
+	 * Create a table just for extended statistics
+	 */
+	result = PQexec(conn,
+		"CREATE TEMPORARY TABLE import_ext_stats ( "
+		"id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, "
+		"schemaname text, "
+		"relname text, "
+		"ext_stats_name text, "
+		"server_version_num integer, "
+		"stats jsonb )");
+
+	if (PQresultStatus(result) != PGRES_COMMAND_OK)
+		pg_fatal("could not create temporary table: %s", PQerrorMessage(conn));
+
+	PQclear(result);
+
+	/*
+	 * Copy input data into combined table.
+	 */
+	result = PQexec(conn,
+		"COPY import_stats FROM STDIN");
+
+	if (PQresultStatus(result) != PGRES_COPY_IN)
+		pg_fatal("error copying data to import_stats: %s", PQerrorMessage(conn));
+
+	for (;;)
+	{
+		char copybuf[COPY_BUF_LEN];
+
+		int numread = fread(copybuf, 1, COPY_BUF_LEN, copysrc);
+
+		if (ferror(copysrc))
+			pg_fatal("error reading from source");
+
+		if (numread == 0)
+			break;
+
+		if (PQputCopyData(conn, copybuf, numread) == -1)
+			pg_fatal("eror during copy: %s", PQerrorMessage(conn));
+	}
+
+	if (PQputCopyEnd(conn, NULL) == -1)
+		pg_fatal("eror during copy: %s", PQerrorMessage(conn));
+	fclose(copysrc);
+
+	result = PQgetResult(conn);
+
+	if (PQresultStatus(result) != PGRES_COMMAND_OK)
+		pg_fatal("error copying data to import_stats: %s", PQerrorMessage(conn));
+
+	PQclear(result);
+
+	/*
+	 * Insert rel stats into their own table with numbering.
+	 */
+	result = PQexec(conn,
+		"INSERT INTO import_rel_stats(schemaname, relname, server_version_num, "
+		"stats) "
+		"SELECT schemaname, relname, server_version_num, stats "
+		"FROM import_stats "
+		"WHERE ext_stats_name IS NULL ");
+
+	if (PQresultStatus(result) != PGRES_COMMAND_OK)
+		pg_fatal("relation stats insert error: %s", PQerrorMessage(conn));
+
+	numtables = atol(PQcmdTuples(result));
+
+	PQclear(result);
+
+	/*
+	 * Insert extended stats into their own table with numbering.
+	 */
+	result = PQexec(conn,
+		"INSERT INTO import_ext_stats(schemaname, relname, ext_stats_name, "
+		"server_version_num, stats) "
+		"SELECT schemaname, relname, ext_stats_name, server_version_num, "
+		"stats "
+		"FROM import_stats "
+		"WHERE ext_stats_name IS NOT NULL ");
+
+	if (PQresultStatus(result) != PGRES_COMMAND_OK)
+		pg_fatal("relation stats insert error: %s", PQerrorMessage(conn));
+
+	numextstats = atol(PQcmdTuples(result));
+
+	PQclear(result);
+
+	if (numtables > 0)
+	{
+
+		result = PQprepare(conn, "import_rel",
+			"SELECT pg_import_rel_stats(c.oid, s.stats, true, true) AS import_result "
+			"FROM import_rel_stats AS s "
+			"JOIN pg_namespace AS n ON n.nspname = s.schemaname "
+			"JOIN pg_class AS c ON c.relnamespace = n.oid "
+			"                   AND c.relname = s.relname "
+			"WHERE s.id = $1::bigint ",
+			1, NULL);
+
+		if (PQresultStatus(result) != PGRES_COMMAND_OK)
+			pg_fatal("error in PREPARE: %s", PQerrorMessage(conn));
+
+		PQclear(result);
+
+		if (!quiet)
+		{
+			result = PQprepare(conn, "echo_rel",
+				"SELECT s.schemaname, s.relname "
+				"FROM import_rel_stats AS s "
+				"WHERE s.id = $1::bigint ",
+				1, NULL);
+
+			if (PQresultStatus(result) != PGRES_COMMAND_OK)
+				pg_fatal("error in PREPARE: %s", PQerrorMessage(conn));
+
+			PQclear(result);
+		}
+
+		for (i = 1; i <= numtables; i++)
+		{
+			char	istr[32];
+			char   *schema = NULL;
+			char   *table = NULL;
+
+			const char *const values[] = {istr};
+
+			snprintf(istr, 32, "%d", i);
+
+			if (!quiet)
+			{
+				result = PQexecPrepared(conn, "echo_rel", 1, values, NULL, NULL, 0);
+				schema = pg_strdup(PQgetvalue(result, 0, 0));
+				table = pg_strdup(PQgetvalue(result, 0, 1));
+			}
+
+			PQclear(result);
+
+			result = PQexecPrepared(conn, "import_rel", 1, values, NULL, NULL, 0);
+
+			if (quiet)
+			{
+				PQclear(result);
+				continue;
+			}
+
+			if (PQresultStatus(result) == PGRES_TUPLES_OK)
+			{
+				int 	rows = PQntuples(result);
+
+				if (rows == 1)
+				{
+					char   *retval = PQgetvalue(result, 0, 0);
+					if (*retval == 't')
+						printf("%s.%s: imported\n", schema, table);
+					else
+						printf("%s.%s: failed\n", schema, table);
+				}
+				else if (rows == 0)
+					printf("%s.%s: not found\n", schema, table);
+				else
+					pg_fatal("import function must return 0 or 1 rows");
+			}
+			else
+				printf("%s.%s: error: %s\n", schema, table, PQerrorMessage(conn));
+
+			if (schema != NULL)
+				pfree(schema);
+
+			if (table != NULL)
+				pfree(table);
+
+			PQclear(result);
+		}
+	}
+
+	if (numextstats > 0)
+	{
+
+	result = PQprepare(conn, "import_ext",
+		"SELECT pg_import_ext_stats(e.oid, s.stats, true, true) AS import_result "
+		"FROM import_ext_stats AS s "
+		"JOIN pg_namespace AS n ON n.nspname = s.schemaname "
+		"JOIN pg_class AS c ON c.relnamespace = n.oid "
+		"                   AND c.relname = s.relname "
+		"JOIN pg_statistic_ext AS e ON e.stxrelid = c.oid "
+		"                   AND e.stxname = s.ext_stats_name "
+		"WHERE s.id = $1::bigint "
+		"AND false ", /* remove when we enable extended stats */
+		1, NULL);
+
+		if (PQresultStatus(result) != PGRES_COMMAND_OK)
+			pg_fatal("error in PREPARE: %s", PQerrorMessage(conn));
+
+		PQclear(result);
+
+		if (!quiet)
+		{
+			result = PQprepare(conn, "echo_ext",
+				"SELECT s.schemaname, s.relname, s.ext_stats_name "
+				"FROM import_ext_stats AS s "
+				"WHERE s.id = $1::bigint ",
+				1, NULL);
+
+			if (PQresultStatus(result) != PGRES_COMMAND_OK)
+				pg_fatal("error in PREPARE: %s", PQerrorMessage(conn));
+
+			PQclear(result);
+		}
+
+		for (i = 1; i <= numextstats; i++)
+		{
+			char	istr[32];
+			char   *schema = NULL;
+			char   *table = NULL;
+			char   *stat = NULL;
+
+			const char *const values[] = {istr};
+
+			snprintf(istr, 32, "%d", i);
+
+			if (!quiet)
+			{
+				result = PQexecPrepared(conn, "echo_ext", 1, values, NULL, NULL, 0);
+				schema = pg_strdup(PQgetvalue(result, 0, 0));
+				table = pg_strdup(PQgetvalue(result, 0, 1));
+				stat = pg_strdup(PQgetvalue(result, 0, 2));
+			}
+
+			PQclear(result);
+
+			result = PQexecPrepared(conn, "import_ext", 1, values, NULL, NULL, 0);
+
+			if (quiet)
+			{
+				PQclear(result);
+				continue;
+			}
+
+			if (PQresultStatus(result) == PGRES_TUPLES_OK)
+			{
+				int 	rows = PQntuples(result);
+
+				if (rows == 1)
+				{
+					char   *retval = PQgetvalue(result, 0, 0);
+					if (*retval == 't')
+						printf("%s on %s.%s: imported\n", stat, schema, table);
+					else
+						printf("%s on %s.%s: failed\n", stat, schema, table);
+				}
+				else if (rows == 0)
+					printf("%s on %s.%s: not found\n", stat, schema, table);
+				else
+					pg_fatal("import function must return 0 or 1 rows");
+			}
+			else
+				printf("%s on %s.%s: error: %s\n", stat, schema, table, PQerrorMessage(conn));
+
+			if (schema != NULL)
+				pfree(schema);
+
+			if (table != NULL)
+				pfree(table);
+
+			if (stat != NULL)
+				pfree(stat);
+
+			PQclear(result);
+		}
+	}
+
+	exit(0);
+}
+
+
+static void
+help(const char *progname)
+{
+	printf(_("%s clusters all previously clustered tables in a database.\n\n"), progname);
+	printf(_("Usage:\n"));
+	printf(_("  %s [OPTION]... [DBNAME]\n"), progname);
+	printf(_("\nOptions:\n"));
+	printf(_("  -d, --dbname=DBNAME       database to cluster\n"));
+	printf(_("  -q, --quiet               don't write any messages\n"));
+	printf(_("  -t, --table=TABLE         cluster specific table(s) only\n"));
+	printf(_("  -V, --version             output version information, then exit\n"));
+	printf(_("  -?, --help                show this help, then exit\n"));
+	printf(_("\nConnection options:\n"));
+	printf(_("  -h, --host=HOSTNAME       database server host or socket directory\n"));
+	printf(_("  -p, --port=PORT           database server port\n"));
+	printf(_("  -U, --username=USERNAME   user name to connect as\n"));
+	printf(_("  -w, --no-password         never prompt for password\n"));
+	printf(_("  -W, --password            force password prompt\n"));
+	printf(_("  --maintenance-db=DBNAME   alternate maintenance database\n"));
+	printf(_("\nRead the description of the SQL command CLUSTER for details.\n"));
+	printf(_("\nReport bugs to <%s>.\n"), PACKAGE_BUGREPORT);
+	printf(_("%s home page: <%s>\n"), PACKAGE_NAME, PACKAGE_URL);
+}
-- 
2.43.0

---

From: Tomas Vondra <[email-masked]>
Date: Wed, 7 Feb 2024 22:46:52 +0100

Hi,

I took a quick look at the v4 patches. I haven't done much testing yet,
so only some basic review.

0001

- The SGML docs for pg_import_rel_stats may need some changes. It starts
with description of what gets overwritten (non-)transactionally (which
gets repeated twice), but that seems more like an implementation detail.
But it does not really say which pg_class fields get updated. Then it
speculates about the possible use case (pg_upgrade). I think it'd be
better to focus on the overall goal of updating statistics, explain what
gets updated/how, and only then maybe mention the pg_upgrade use case.

Also, it says "statistics are replaced" but it's quite clear if that
applies only to matching statistics or if all stats are deleted first
and then the new stuff is inserted. (FWIW remove_pg_statistics clearly
deletes all pre-existing stats).

- import_pg_statistics: I somewhat dislike that we're passing arguments
as datum[] array - it's hard to say what the elements are expected to
be, etc. Maybe we should expand this, to make it clear. How do we even
know the array is large enough?

- I don't quite understand why we need examine_rel_attribute. It sets a
lot of fields in the VacAttrStats struct, but then we only use attrtypid
and attrtypmod from it - so why bother and not simply load just these
two fields? Or maybe I miss something.

- examine_rel_attribute can return NULL, but get_attrinfo does not check
for NULL and just dereferences the pointer. Surely that can lead to
segfaults?

- validate_no_duplicates and the other validate functions would deserve
a better docs, explaining what exactly is checked (it took me a while to
realize we check just for duplicates), what the parameters do etc.

- Do we want to make the validate_ functions part of the public API? I
realize we want to use them from multiple places (regular and extended
stats), but maybe it'd be better to have an "internal" header file, just
like we have extended_stats_internal?

- I'm not sure we do "\set debug f" elsewhere. It took me a while to
realize why the query outputs are empty ...

0002

- I'd rename create_stat_ext_entry to statext_create_entry.

- Do we even want to include OIDs from the source server? Why not to
just have object names and resolve those? Seems safer - if the target
server has the OID allocated to a different object, that could lead to
confusing / hard to detect issues.

- What happens if we import statistics which includes data for extended
statistics object which does not exist on the target machine?

- pg_import_ext_stats seems to not use require_match_oids - bug?

0003

- no SGML docs for the new tools?

- The help() seems to be wrong / copied from "clusterdb" or something
like that, right?

> of each query suitable for the current server version.

OK

> the old server, and adapt it to the modern structure and local schema

I have no opinion on the proposed format - still JSON, but closer to the
original data. Works for me, but I wonder what Tom thinks about it,
considering he suggested making it closer to the raw data.

> have been dropped, added, or reordered.

Makes sense. What will happen if we try to import data for extended
statistics (or index) that does not exist on the target server?

> the import of that one object if it isn't perfect.

Yeah, column reordering is something we probably need to handle. The
stats order them by attnum, so if we want to allow import on a system
where the attributes were dropped/created in a different way, this is
necessary. I haven't tested this - is there a regression test for this?

I agree expressions are hard. I don't think it's feasible to import
nodetree from other server versions, but why don't we simply deparse the
expression on the source, and either parse it on the target (and then
compare the two nodetrees), or deparse the target too and compare the
two deparsed expressions? I suspect the deparsing may produce slightly
different results on the two versions (causing false mismatches), but
perhaps the deparse on source + parse on target + compare nodetrees
would work? Haven't tried, though.

> Export formats go back to v10.

Do we even want/need to go beyond 12? All earlier versions are EOL.

regards

-- 
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

---

From: Corey Huinker <[email-masked]>
Date: Tue, 13 Feb 2024 00:07:26 -0500

> deletes all pre-existing stats).

All are now deleted first, both in the pg_statistic and
pg_statistic_ext_data tables. The previous version was taking a more
"replace it if we find a new value" approach, but that's overly
complicated, so following the example set by extended statistics seemed
best.

> know the array is large enough?

Completely fair. Initially that was done with the expectation that the
array would be the same for both regular stats and extended stats, but that
was no longer the case.

> two fields? Or maybe I miss something.

I think you're right, we don't need it anymore for regular statistics. We
still need it in extended stats because statext_store() takes a subset of
the vacattrstats rows as an input.

Which leads to a side issue. We currently have 3 functions:
examine_rel_attribute and the two varieties of examine_attribute (one in
analyze.c and the other in extended stats). These are highly similar
but just different enough that I didn't feel comfortable refactoring them
into a one-size-fits-all function, and I was particularly reluctant to
modify existing code for the ANALYZE path.

> for NULL and just dereferences the pointer. Surely that can lead to

Good catch, and it highlights how little we need VacAttrStats for regular
statistics.

> realize we check just for duplicates), what the parameters do etc.

Those functions are in a fairly formative phase - I expect a conversation
about what sort of validations we want to do to ensure that the statistics
being imported make sense, and under what circumstances we would forego
some of those checks.

> like we have extended_stats_internal?

I see no need to have them be a part of the public API. Will move.

> realize why the query outputs are empty ...

That was an experiment that rose out of the difficulty in determining
_where_ a difference was when the set-difference checks failed. So far I
like it, and I'm hoping it catches on.

> confusing / hard to detect issues.

The import functions would obviously never use the imported oids to look up
objects on the destination system. Rather, they're there to verify that the
local object oid matches the exported object oid, which is true in the case
of a binary upgrade.

The export format is an attempt to export the pg_statistic[_ext_data] for
that object as-is, and, as Tom suggested, let the import function do the
transformations. We can of course remove them if they truly have no purpose
for validation.

> statistics object which does not exist on the target machine?

The import function takes an oid of the object (relation or extstat
object), and the json payload is supposed to be the stats for ONE
corresponding object. Multiple objects of data really don't fit into the
json format, and statistics exported for an object that does not exist on
the destination system would have no meaningful invocation. I envision the
dump file looking like this

    CREATE TABLE public.foo (....);

    SELECT pg_import_rel_stats('public.foo'::regclass, <json blob>, option
flag, option flag);

So a call against a nonexistent object would fail on the regclass cast.

> - pg_import_ext_stats seems to not use require_match_oids - bug?

I haven't yet seen a good way to make use of matching oids in extended
stats. Checking matching operator/collation oids would make sense, but
little else.

> - no SGML docs for the new tools?

Correct. I foresee the export tool being folded into pg_dump(), and the
import tool going away entirely as psql could handle it.

> - The help() seems to be wrong / copied from "clusterdb" or something

Correct, for the reason above.

> statistics (or index) that does not exist on the target server?

One of the parameters to the function is the oid of the object that is the
target of the stats. The importer will not seek out objects with matching
names and each JSON payload is limited to holding one object, though
clearly someone could encapsulate the existing format in a format that has
a manifest of objects to import.

> necessary. I haven't tested this - is there a regression test for this?

The overlong transformation SQL starts with the object to be imported (the
local oid was specified) and it

1. grabs all the attributes (or exprs, for extended stats) of that object.
2. looks for columns/exprs in the exported json for an attribute with a
matching name
3. takes the exported attnum of that exported attribute for use in things
like stdexprs
4. looks up the type, collation, and operators for the exported attribute.

So we get a situation where there might not be importable stats for an
attribute of the destination table, and we'd import nothing for that
column. Stats for exported columns with no matching local column would
never be referenced.

Yes, there should be a test of this.

> Do we even want/need to go beyond 12? All earlier versions are EOL.

True, but we had pg_dump and pg_restore stuff back to 7.x until fairly
recently, and a major friction point in getting customers to upgrade their
instances off of unsupported versions is the downtime caused by an upgrade,
why wouldn't we make it easier for them?

---

From: Corey Huinker <[email-masked]>
Date: Thu, 15 Feb 2024 04:09:41 -0500

Posting v5 updates of pg_import_rel_stats() and pg_import_ext_stats(),
which address many of the concerns listed earlier.

Leaving the export/import scripts off for the time being, as they haven't
changed and the next likely change is to fold them into pg_dump.
From 9adcc9735069edc14af05b28088725594e912c84 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Thu, 15 Feb 2024 03:11:40 -0500
Subject: [PATCH v5 1/2] Create pg_import_rel_stats.

This function also allows for tweaking of table statistics in-place,
allowing the user to inflate rowcounts, skew histograms, etc, to see
what those changes will evoke from the query planner.
---
 src/include/catalog/pg_proc.dat               |    6 +-
 src/include/statistics/statistics.h           |    2 +
 src/include/statistics/statistics_internal.h  |   28 +
 src/backend/statistics/Makefile               |    3 +-
 src/backend/statistics/meson.build            |    1 +
 src/backend/statistics/statistics.c           | 1331 +++++++++++++++++
 .../regress/expected/stats_export_import.out  |  530 +++++++
 src/test/regress/parallel_schedule            |    2 +-
 src/test/regress/sql/stats_export_import.sql  |  499 ++++++
 doc/src/sgml/func.sgml                        |   65 +
 10 files changed, 2464 insertions(+), 3 deletions(-)
 create mode 100644 src/include/statistics/statistics_internal.h
 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

index 9c120fc2b7..0e48c08566 100644
index 7f2bf18716..0c3867f918 100644
@@ -127,4 +127,6 @@ extern StatisticExtInfo *choose_best_statistics(List *stats, char requiredkind,
 
+extern Datum pg_import_rel_stats(PG_FUNCTION_ARGS);
+
 #endif							/* STATISTICS_H */
diff --git a/src/include/statistics/statistics_internal.h b/src/include/statistics/statistics_internal.h
new file mode 100644
index 0000000000..e61a64d8b7
--- /dev/null
+++ b/src/include/statistics/statistics_internal.h
@@ -0,0 +1,28 @@
+/*-------------------------------------------------------------------------
+ *
+ * statistics_internal.h
+ *	  Extended statistics and selectivity estimation functions.
+ *
+ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/statistics/statistics_internal.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef STATISTICS_INTERNAL_H
+#define STATISTICS_INTERNAL_H
+
+#include "nodes/pathnodes.h"
+
+extern void validate_no_duplicates(Datum document, bool document_null,
+								   const char *sql, const char *docname,
+								   const char *colname);
+
+extern void validate_exported_types(Datum types, bool types_null);
+extern void validate_exported_collations(Datum collations, bool collations_null);
+extern void validate_exported_operators(Datum operators, bool operators_null);
+extern void validate_exported_attributes(Datum attributes, bool attributes_null);
+extern void validate_exported_statistics(Datum statistics, bool statistics_null);
+
+#endif							/* STATISTICS_INTERNAL_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
 
index 0000000000..25ae54d4e8
@@ -0,0 +1,1331 @@
+#include "statistics/statistics_internal.h"
+ * get_attrinfo
+	Oid		collid;
+		Form_pg_attribute attr = TupleDescAttr(rel->rd_att, i);
+		 * of the list. We need to do this even if the attribute is
+		 * dropped to pop a potential expression off the list.
+			res[i].typid = exprType(index_expr);
+			res[i].typmod = exprTypmod(index_expr);
+			if (OidIsValid(attr->attcollation))
+				res[i].collid = attr->attcollation;
+				res[i].collid = exprCollation(index_expr);
+			res[i].typid = attr->atttypid;
+			res[i].typmod = attr->atttypmod;
+			res[i].collid = attr->attcollation;
+		res[i].basetypid = get_base_element_type(res[i].typid);
+ * Find any duplicate values in a jsonb object casted via SPI SQL
+ * into a single-key table.
+ * Presently the only check we make is to ensure that no duplicate oid values
+ * exist in the expanded table.
+ * Presently the only check we make is to ensure that no duplicate oid values
+ * exist in the expanded table.
+ * Presently the only check we make is to ensure that no duplicate oid values
+ * exist in the expanded table.
+ * Presently the only check we make is to ensure that no duplicate attnum
+ * values exist in the expanded table.
+ * Presently the only check we make is to ensure that no duplicate combinations
+ * of (staattnum, stainherit) exist within the expanded table.
+static HeapTuple *
+					 Datum types_datum, bool types_null,
+					 Datum collations_datum, bool collations_null,
+					 Datum operators_datum, bool operators_null,
+					 Datum attributes_datum, bool attributes_null,
+					 Datum statistics_datum, bool statistics_null,
+					types_datum, collations_datum, operators_datum,
+					attributes_datum, statistics_datum,
+					NULLARG(types_null), NULLARG(collations_null),
+					NULLARG(operators_null), NULLARG(attributes_null),
+					NULLARG(statistics_null), NULLARG(false) };
+									datums[BQ_TYPES], nulls[BQ_TYPES],
+									datums[BQ_COLLATIONS], nulls[BQ_COLLATIONS],
+									datums[BQ_OPERATORS], nulls[BQ_OPERATORS],
+									datums[BQ_ATTRIBUTES], nulls[BQ_ATTRIBUTES],
+									datums[BQ_STATISTICS], nulls[BQ_STATISTICS],
 
 # ----------
index cf3de80394..2be0a30d4d 100644
@@ -28732,6 +28732,71 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
 
+   <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_import_rel_stats</primary>
+        </indexterm>
+        <function>pg_import_rel_stats</function> ( <parameter>relation</parameter> <type>regclass</type>, <parameter>relation_stats</parameter> <type>jsonb</type>, <parameter>validate</parameter> <type>bool</type>, <parameter>require_match_oids</parameter> <type>bool</type> )
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Replaces all statistics generated by a previous
+        <command>ANALYZE</command> for the <parameter>relation</parameter>
+        with values specified in <parameter>relation_stats</parameter>.
+       </para>
+       <para>
+        Specifically, the <structname>pg_statistic</structname> rows with a
+        <structfield>statrelid</structfield> matching
+        <parameter>relation</parameter> are replaced with the values derived
+        from <parameter>relation_stats</parameter>, and the
+        <structname>pg_class</structname> entry for
+        <parameter>relation</parameter> is modified, replacing the
+        <structfield>reltuples</structfield> and
+        <structfield>relpages</structfield> with values found in
+        <parameter>relation_stats</parameter>.
+       </para>
+       <para>
+        The purpose of this function is to apply statistics values in an
+        upgrade situation that are "good enough" for system operation until
+        they are replaced by the next auto-analyze. This function
+        could be 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>
+        If <parameter>validate</parameter> is set to <literal>true</literal>,
+        then the function will perform a series of data consistency checks on
+        the data in <parameter>relation_stats</parameter> before attempting to
+        import statistics. Any inconsistencies found will raise an error.
+       </para>
+       <para>
+        If <parameter>require_match_oids</parameter> is set to <literal>true</literal>,
+        then the import will fail if the imported oids for <structname>pt_type</structname>,
+        <structname>pg_collation</structname>, and <structname>pg_operator</structname> do
+        not match the values specified in <parameter>relation_json</parameter>, as would be expected
+        in a binary upgrade. These assumptions would not be true when restoring from a dump.
+       </para></entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
    <table id="functions-admin-dblocation">
     <title>Database Object Location Functions</title>
     <tgroup cols="1">
-- 
2.43.0

From c0567d9927055e5de6fe5bef008931e7e1de9c42 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Thu, 15 Feb 2024 03:36:30 -0500
Subject: [PATCH v5 2/2] Create pg_import_ext_stats().

This is the extended statistics equivalent of pg_import_rel_stats().

This function also allows for tweaking of table statistics in-place,
allowing the user to simulate correlations, skew histograms, etc, to see
what those changes will evoke from the query planner.
---
 src/include/catalog/pg_proc.dat               |   5 +
 .../statistics/extended_stats_internal.h      |   7 +
 src/backend/statistics/dependencies.c         | 161 +++
 src/backend/statistics/extended_stats.c       | 986 ++++++++++++++++--
 src/backend/statistics/mcv.c                  | 192 ++++
 src/backend/statistics/mvdistinct.c           | 160 +++
 .../regress/expected/stats_export_import.out  | 265 ++++-
 src/test/regress/sql/stats_export_import.sql  | 245 ++++-
 doc/src/sgml/func.sgml                        |  28 +-
 9 files changed, 1976 insertions(+), 73 deletions(-)

index 0e48c08566..701ed3a2c9 100644

 

 	return s1;
 }
+
+/*
index c5461514d8..841c3d8f55 100644
@@ -32,10 +34,12 @@
+#include "statistics/statistics_internal.h"
 #include "utils/acl.h"
 #include "utils/array.h"
 #include "utils/attoptcache.h"
@@ -418,6 +422,83 @@ statext_is_kind_built(HeapTuple htup, char type)
 
+/*
+statext_create_entry(HeapTuple htup)
@@ -443,74 +524,7 @@ fetch_statentries_for_relation(Relation pg_statext, Oid relid)
 
+		StatExtEntry *entry = statext_create_entry(htup);
 
@@ -2636,3 +2650,839 @@ make_build_data(Relation rel, StatExtEntry *stat, int numrows, HeapTuple *rows,
 
 	return result;
 }
+
+/*
+static VacAttrStats *
+	stxentry = statext_create_entry(etup);
 
 	return s;
 }
+
+/*
+ * statext_mcv_import
+ *
 
 	pfree(current);
 }
+
+/*

+SELECT
+    jsonb_build_object(

 

 
 
+SELECT
+    jsonb_build_object(

 
 
index 2be0a30d4d..6f41c7a292 100644
@@ -28787,12 +28787,38 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset